commit fb6a79296585e92b0bd7e4d3e0b174cdf02755dd Author: Romain <> Date: Sun Feb 21 20:01:13 2021 +0100 Snapshot 23797f5059 diff --git a/Licence note b/Licence note new file mode 100644 index 0000000..01464de --- /dev/null +++ b/Licence note @@ -0,0 +1 @@ +If you find software that doesn’t have a license, that means you have no permission from the creators of the software to use, modify, or share the software. Although a code host such as GitHub may allow you to view and fork the code, this does not imply that you are permitted to use, modify, or share the software for any purpose. diff --git a/README.md b/README.md new file mode 100644 index 0000000..da43e24 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# CommandPC + +CommandPC is a program for executing commands on a remote machine regardless of the operating system. + +## Description + +The too long didn't read version is that: it's like SSH, but less secure and just a fun experimentation with threads, internationalization, encryption and GUI in JAVA. + +Composed of two part, a client and a server, this application allow you to send encrypted command from one machine to another. Both the client and the server can operate with or without a gui. +English and French language are disponible within the application. + +## Instruction + +### Server + +Without the `nogui` argument, the server display its status with a system tray icon. By right-clicking the icon you can interact with it. + +| Server started | Server stopped | +|:--------------:|:--------------:| +|![server-systemTray-icon](img/CommandPC_systemTrayIcon.png)|![server-systemTray-icon](img/CommandPC_systemTrayIcon2.png)||| + +Example: + +- Windows sytem tray: ![server-systemTray-icon](img/CommandPC_systemTrayWindows.png) +- Linux XFCE system tray: ![server-systemTray-icon](img/CommandPC_systemTrayLinux.png) + +Right-clicking on the icon allow you to start and stop the server. The command line argument `port=X` chand the listening port. + +### Client + +The client use a graphical interface to guide the user and is available in both French and English (using the "Français"/"English" button at the bottom) +The interface is composed of two pane: "connection" and "command". + +The "connection" pane allows the connection to a remote server. + +![connection-result](img/CommandPC_connectionResult.png) + +After the connection established, the "command" pane allow to send commands to the server. + +| 'ls /' on a Linux machine | 'ipconfig' on a Windows machine and gui in French for a change | +|:-------------------------:|:-------------------------------:| +|![ls](img/CommandPC_commandLs.png)|![ipconfig](img/CommandPC_commandIpconfig.png)| + +## Security concerns + +**TLDR: Use at your own risk over a secure network.** + +### Command + +No effort is made to check if the command that you send is compatible with the user privilige, exist or is even safe to use. + +### Identity + +No identification of the user or the machine is made. In other words, if a server is running any number of client can connect and run command on it. + +### Encryption + +This software use AES 128 bits encryption, but like too many software (including payed one) it has flaws that make it unsuitable to use over unsecured networks. + +The main issue is the fact that during the first communication between the client and the server, the communication is encrypted via a master key. +This master key, is the same for all firsts exchanges. After that, a new communication key is generated between individual client and the server. +If somebody were to use a packet sniffer on the network, knowing the master key it's possible to get the communication key thus defeating the encryption entirely. + +Side note, AES 128 allow this program to run on any implementation of the Java platform. See the [Cipher documentation](https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html) for the complete list of "safe to use" cipher standard. diff --git a/client/.classpath b/client/.classpath new file mode 100644 index 0000000..fd51525 --- /dev/null +++ b/client/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..ed9b2c9 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,91 @@ +######################## +#JAVA +######################## +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +######################## +# ECLIPSE +######################## +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + diff --git a/client/.project b/client/.project new file mode 100644 index 0000000..1b0fe6e --- /dev/null +++ b/client/.project @@ -0,0 +1,17 @@ + + + client + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/client/resource/iconCommandPC.png b/client/resource/iconCommandPC.png new file mode 100644 index 0000000..7fa2e8e Binary files /dev/null and b/client/resource/iconCommandPC.png differ diff --git a/client/resource/iconCommunication.png b/client/resource/iconCommunication.png new file mode 100644 index 0000000..5fed72c Binary files /dev/null and b/client/resource/iconCommunication.png differ diff --git a/client/resource/iconConnection.png b/client/resource/iconConnection.png new file mode 100644 index 0000000..472d8a8 Binary files /dev/null and b/client/resource/iconConnection.png differ diff --git a/client/src/bundle/Bundle.properties b/client/src/bundle/Bundle.properties new file mode 100644 index 0000000..ffa92f3 --- /dev/null +++ b/client/src/bundle/Bundle.properties @@ -0,0 +1,38 @@ + +address = Address + +clear = Clear + +command = Command + +commandToExecute = Command to execute: + +connected = Connected + +connectedTo = Connected to + +connection = Connection + +connectionEnded = Connection ended + +errorSpecifyAddressAndPort = Specify both address and port. + +execute = Execute + +help_argument = Arguments: + +help_cmd = Send a command X to the remote server. Don't launch the graphical user interface. + +help_help = Show this help message. + +help_intro = CommandPC is a program for executing commands on a remote machine regardless of the operating system. + +help_ip = Set the server address to X. + +help_port = Set the server port to X. + +languageAlt = Fran\u00E7ais + +languageName = English + +port = Port diff --git a/client/src/bundle/Bundle_fr.properties b/client/src/bundle/Bundle_fr.properties new file mode 100644 index 0000000..b120855 --- /dev/null +++ b/client/src/bundle/Bundle_fr.properties @@ -0,0 +1,38 @@ + +address = Adresse + +clear = Vider + +command = Commande + +commandToExecute = Commande \u00E0 ex\u00E9cuter: + +connected = Connect\u00E9 + +connectedTo = Connect\u00E9 \u00E0 + +connection = Connexion + +connectionEnded = Connexion termin\u00E9e + +errorSpecifyAddressAndPort = Pr\u00E9cisez l'adresse et le port. + +execute = Ex\u00E9cuter + +help_argument = Arguments: + +help_cmd = Envoie une commande X au serveur distant. Ne d\u00E9marre pas l'interface graphique. + +help_help = Affiche ce message d'aide. + +help_intro = CommandPC est un programme destin\u00E9 \u00E0 ex\u00E9cuter des commandes sur un serveur distant, quelque soit son syst\u00E8me d'exploitation. + +help_ip = D\u00E9fini l'adresse du serveur distant. + +help_port = D\u00E9fini le port du serveur distant. + +languageAlt = English + +languageName = Fran\u00E7ais + +port = Port diff --git a/client/src/generic/Status.java b/client/src/generic/Status.java new file mode 100644 index 0000000..abfdaf0 --- /dev/null +++ b/client/src/generic/Status.java @@ -0,0 +1,53 @@ +package generic; + +/** + * Generic response class. + */ +public class Status { + /** Indicate if the operation was a success. By default, false. */ + public boolean success; + /** Response message.*/ + public String message; + /** Response payload.*/ + public T payload; + /** Response error */ + public E error; + + public Status() { + super(); + this.success=false; + } + public Status(boolean success) { + super(); + this.success = success; + } + public Status(boolean success, String message) { + super(); + this.success = success; + this.message = message; + } + public Status(boolean success, String message, T payload) { + super(); + this.success = success; + this.message = message; + this.payload = payload; + } + public Status(boolean success, String message, T payload, E error) { + super(); + this.success = success; + this.message = message; + this.payload = payload; + this.error = error; + } + @Override + public String toString() { + String result="Status [success="+success+", message="+message; + if(payload!=null) { + result+=", payload="+payload.toString(); + } + if(error!=null) { + result+=", error="+error.toString(); + } + return result+"]"; + } +} \ No newline at end of file diff --git a/client/src/gui/GUI.java b/client/src/gui/GUI.java new file mode 100644 index 0000000..c44ae5b --- /dev/null +++ b/client/src/gui/GUI.java @@ -0,0 +1,173 @@ +package gui; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.SwingConstants; +import javax.swing.WindowConstants; + +import generic.Status; +import gui.panel.CommunicationPanel; +import gui.panel.ConnectionPanel; +import network.CommunicationManager; + +public class GUI extends JFrame { + private static final long serialVersionUID = 1L; + private ConnectionPanel connectionPanel; + private CommunicationPanel communicationPanel; + private JPanel languagePanel; + private JButton languageButton; + private JTabbedPane tabbedPane; + + private CommunicationManager communicationManager; + + public GUI(CommunicationManager communicationManager) { + super(); + this.communicationManager=communicationManager; + connectionPanel=new ConnectionPanel(this); + connectionPanel.setAddress(communicationManager.getAddress()); + connectionPanel.setPort(String.valueOf(communicationManager.getPort())); + communicationPanel=new CommunicationPanel(this); + initialize(); + } + + /** + * Initialize the default state of the different GUI elements. + */ + private void initialize() { + this.setTitle("CommandPC - MARTIN Romain"); + this.setIconImage(getToolkit().getImage(getClass().getResource("/iconCommandPC.png"))); + this.setSize(600,300); + this.setResizable(true); + + this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + this.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(java.awt.event.WindowEvent e) { + communicationManager.disconnect(); + dispose(); + }; + }); + + GridBagLayout gbl_contentPane=new GridBagLayout(); + getContentPane().setLayout(gbl_contentPane); + + tabbedPane=new JTabbedPane(SwingConstants.TOP); + tabbedPane.add("panelConnection",connectionPanel); + tabbedPane.add("panelCommunication",communicationPanel); + tabbedPane.setIconAt(0,new ImageIcon(getClass().getResource("/iconConnection.png"))); + tabbedPane.setIconAt(1,new ImageIcon(getClass().getResource("/iconCommunication.png"))); + tabbedPane.setEnabledAt(1,false); + GridBagConstraints gbc_tabbedPane=new GridBagConstraints(); + gbc_tabbedPane.gridx=0; + gbc_tabbedPane.gridy=0; + gbc_tabbedPane.weightx=1.0d; + gbc_tabbedPane.weighty=1.0d; + gbc_tabbedPane.fill=GridBagConstraints.BOTH; + getContentPane().add(tabbedPane,gbc_tabbedPane); + + GridBagLayout gbl_languagePanel=new GridBagLayout(); + languagePanel=new JPanel(gbl_languagePanel); + GridBagConstraints gbc_languagePanel=new GridBagConstraints(); + gbc_languagePanel.gridx=0; + gbc_languagePanel.gridy=1; + gbc_languagePanel.fill=GridBagConstraints.HORIZONTAL; + + languageButton=new JButton("languageButton"); + languageButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + if(Locale.getDefault().getLanguage().equals(Locale.FRENCH.toString())) { + Locale.setDefault(Locale.ENGLISH); + }else { + Locale.setDefault(Locale.FRENCH); + } + //ResourceBundle.clearCache(); + setText(); + } + }); + languagePanel.add(languageButton); + + getContentPane().add(languagePanel,gbc_languagePanel); + + setText(); + } + + /** + * Set the correct text for all graphical elements according to the default {@link Locale}. + * @see java.util.Locale#getDefault() + */ + private void setText(){ + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + languageButton.setText(resourceBundle.getString("languageAlt")); + tabbedPane.setTitleAt(0,resourceBundle.getString("connection")); + tabbedPane.setTitleAt(1,resourceBundle.getString("command")); + + communicationPanel.setText(); + connectionPanel.setText(); + } + + /** + * Request connection status change. + * @param isConnection true if this is a connection request, false to disconnect. + * @param address address of the remote server. + * @param port port of the remote server. + * @return true if operation successful. + */ + public boolean changeConnectionStatus(boolean isConnection,String address,String port) { + //Disconnection + if(isConnection==false) { + communicationManager.disconnect(); + tabbedPane.setEnabledAt(1,false); + tabbedPane.setSelectedIndex(0); + return true; + } + + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + + if(address.isBlank() || port.isBlank()) { + connectionPanel.addLog(resourceBundle.getString("errorSpecifyAddressAndPort"),true); + return false; + } + + Status result=communicationManager.connect(address,Integer.valueOf(port)); + if(result.success) { + connectionPanel.addLog(resourceBundle.getString("connectedTo")+" "+result.message.toString(),false); + tabbedPane.setEnabledAt(1,true); + tabbedPane.setSelectedIndex(1); + } else { + connectionPanel.addLog(result.error.toString(),true); + } + + return result.success; + } + + /** + * Send command to the server and receive his answer. + * @param text command to send. + */ + public void sendCommand(String text) { + Status resultSend=communicationManager.encryptAndSendString(text); + if(resultSend.success==false) { + communicationPanel.addLog(resultSend.error.toString(), true); + return; + } + Status resultReceive=communicationManager.receiveAndDecryptString(); + if(resultReceive.success) { + if(resultReceive.payload.trim().startsWith("java")) { + communicationPanel.addLog(resultReceive.payload.trim(), true); + } else { + communicationPanel.addLog(resultReceive.payload.trim(), false); + } + } else { + communicationPanel.addLog(resultReceive.error.toString(), true); + } + } +} diff --git a/client/src/gui/panel/CommunicationPanel.java b/client/src/gui/panel/CommunicationPanel.java new file mode 100644 index 0000000..dd32e56 --- /dev/null +++ b/client/src/gui/panel/CommunicationPanel.java @@ -0,0 +1,85 @@ +package gui.panel; + +import java.awt.GridBagConstraints; +import java.awt.event.KeyEvent; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JTextField; + +import gui.GUI; + +/** + *

Handle the communication with the server

+ *

Allow the user to input and send commands and display their results.

+ */ +public class CommunicationPanel extends LogPanel { + private static final long serialVersionUID = 1L; + private JLabel commandLabel; + private JTextField commandText; + private JButton executeButton; + + private GUI gui; + + public CommunicationPanel(GUI gui) { + super(); + this.gui=gui; + initialize(); + } + + @Override + protected void initialize() { + super.initialize(); + //GridBagLayout gbl_main=new GridBagLayout(); + //this.setLayout(gbl_main); + + commandLabel=new JLabel("labelCommand"); + GridBagConstraints gbc_commandLabel=new GridBagConstraints(); + gbc_commandLabel.gridx=0; + gbc_commandLabel.gridy=0; + gbc_commandLabel.anchor=GridBagConstraints.WEST; + this.add(commandLabel,gbc_commandLabel); + + commandText=new JTextField(); + commandText.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER){ + executeButton.doClick(); + } + } + }); + GridBagConstraints gbc_commandText=new GridBagConstraints(); + gbc_commandText.gridx=0; + gbc_commandText.gridy=1; + gbc_commandText.weightx=1.0d; + gbc_commandText.fill=GridBagConstraints.BOTH; + this.add(commandText,gbc_commandText); + + executeButton=new JButton("executeButton"); + executeButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + if(commandText.getText().isBlank()==false) { + gui.sendCommand(commandText.getText()); + } + } + }); + GridBagConstraints gbc_executeButton=new GridBagConstraints(); + gbc_executeButton.gridx=3; + gbc_executeButton.gridy=1; + this.add(executeButton,gbc_executeButton); + + setText(); + } + + @Override + public void setText() { + super.setText(); + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + commandLabel.setText(resourceBundle.getString("commandToExecute")); + executeButton.setText(resourceBundle.getString("execute")); + } +} \ No newline at end of file diff --git a/client/src/gui/panel/ConnectionPanel.java b/client/src/gui/panel/ConnectionPanel.java new file mode 100644 index 0000000..107d79b --- /dev/null +++ b/client/src/gui/panel/ConnectionPanel.java @@ -0,0 +1,145 @@ +package gui.panel; + +import java.awt.GridBagConstraints; +import java.awt.event.KeyEvent; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; + +import gui.GUI; + +/** + * Handle connection state, allowing to set up and establish a connection as well as displaying it status. + */ +public class ConnectionPanel extends LogPanel { + private static final long serialVersionUID = 1L; + private JLabel addressLabel; + private JTextField addressText; + private JLabel portLabel; + private JTextField portText; + private JLabel addressPortSepartatorLabel; + private JToggleButton connectionToggleButton; + + private GUI gui; + + public void setAddress(String address) { + if(addressText!=null) addressText.setText(address); + } + public void setPort(String port) { + if(portText!=null) portText.setText(port); + } + + public ConnectionPanel(GUI gui) { + super(); + this.gui=gui; + initialize(); + } + + @Override + protected void initialize() { + super.initialize(); + /*GridBagLayout gbl_main=new GridBagLayout(); + this.setLayout(gbl_main);*/ + + addressLabel=new JLabel("addressLabel"); + GridBagConstraints gbc_addressLabel=new GridBagConstraints(); + gbc_addressLabel.gridx=0; + gbc_addressLabel.gridy=0; + this.add(addressLabel,gbc_addressLabel); + + addressText=new JTextField(); + addressText.setHorizontalAlignment(SwingConstants.CENTER); + addressText.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER){ + if(connectionToggleButton.isSelected()==false) { + connectionToggleButton.doClick(); + } + } + } + }); + GridBagConstraints gbc_addressText=new GridBagConstraints(); + gbc_addressText.gridx=0; + gbc_addressText.gridy=1; + gbc_addressText.weightx=0.5d; + gbc_addressText.fill=GridBagConstraints.BOTH; + this.add(addressText,gbc_addressText); + + addressPortSepartatorLabel=new JLabel(" : "); + addressPortSepartatorLabel.setHorizontalAlignment(SwingConstants.CENTER); + GridBagConstraints gbc_addressPortSepartatorLabel=new GridBagConstraints(); + gbc_addressPortSepartatorLabel.gridx=1; + gbc_addressPortSepartatorLabel.gridy=1; + this.add(addressPortSepartatorLabel,gbc_addressPortSepartatorLabel); + + portLabel=new JLabel("portLabel"); + GridBagConstraints gbc_portLabel=new GridBagConstraints(); + gbc_portLabel.gridx=2; + gbc_portLabel.gridy=0; + this.add(portLabel,gbc_portLabel); + + portText=new JTextField(); + portText.setHorizontalAlignment(SwingConstants.CENTER); + portText.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyPressed(java.awt.event.KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER){ + if(connectionToggleButton.isSelected()==false) { + connectionToggleButton.doClick(); + } + } + } + }); + GridBagConstraints gbc_portText=new GridBagConstraints(); + gbc_portText.gridx=2; + gbc_portText.gridy=1; + gbc_portText.weightx=0.5d; + gbc_portText.fill=GridBagConstraints.BOTH; + this.add(portText,gbc_portText); + + connectionToggleButton=new JToggleButton("connectionToggleButton"); + connectionToggleButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + boolean success=gui.changeConnectionStatus(connectionToggleButton.isSelected(),addressText.getText(),portText.getText()); + if(success==false) { + connectionToggleButton.setSelected(false); + } + setTextConnectionToggleButton(); + } + }); + GridBagConstraints gbc_connectionToggleButton=new GridBagConstraints(); + gbc_connectionToggleButton.gridx=3; + gbc_connectionToggleButton.gridy=1; + gbc_connectionToggleButton.fill=GridBagConstraints.HORIZONTAL; + this.add(connectionToggleButton,gbc_connectionToggleButton); + + setText(); + } + + /** + * Set the correct text for toggleButtonConnection regarding the selected language and it's status. + */ + private void setTextConnectionToggleButton() { + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + if(connectionToggleButton.isSelected()) { + connectionToggleButton.setText(resourceBundle.getString("connected")); + }else { + connectionToggleButton.setText(resourceBundle.getString("connection")); + } + } + + @Override + public void setText() { + super.setText(); + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + addressLabel.setText(resourceBundle.getString("address")); + portLabel.setText(resourceBundle.getString("port")); + setTextConnectionToggleButton(); + } +} diff --git a/client/src/gui/panel/LogPanel.java b/client/src/gui/panel/LogPanel.java new file mode 100644 index 0000000..851ca62 --- /dev/null +++ b/client/src/gui/panel/LogPanel.java @@ -0,0 +1,97 @@ +package gui.panel; + +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +public class LogPanel extends JPanel { + private static final long serialVersionUID = 1L; + private JTextPane logTextPane; + private StyledDocument logStyleDocument; + private SimpleAttributeSet normalAttributeSet; + private SimpleAttributeSet errorAttributeSet; + private JScrollPane logScrollPane; + private JButton clearLogButton; + + public LogPanel( ) { + super(); + normalAttributeSet=new SimpleAttributeSet(); + normalAttributeSet.addAttribute(StyleConstants.Foreground,Color.black); + errorAttributeSet=new SimpleAttributeSet(); + errorAttributeSet.addAttribute(StyleConstants.Foreground,Color.red); + // initialize(); + } + + /** + * Initialize the default state of the different GUI elements. + */ + protected void initialize() { + GridBagLayout gbl_main=new GridBagLayout(); + this.setLayout(gbl_main); + + logTextPane=new JTextPane(); + logStyleDocument=logTextPane.getStyledDocument(); + logScrollPane=new JScrollPane(logTextPane); + GridBagConstraints gbc_logScrollPane=new GridBagConstraints(); + gbc_logScrollPane.gridx=0; + gbc_logScrollPane.gridy=2; + gbc_logScrollPane.weightx=1.0d; + gbc_logScrollPane.weighty=1.0d; + gbc_logScrollPane.gridwidth=4; + gbc_logScrollPane.fill=GridBagConstraints.BOTH; + gbc_logScrollPane.insets=new Insets(5,0,0,0); + this.add(logScrollPane,gbc_logScrollPane); + + clearLogButton=new JButton("clearLogButton"); + clearLogButton.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + logTextPane.setText(""); + } + }); + GridBagConstraints gbc_clearLogButton=new GridBagConstraints(); + gbc_clearLogButton.gridx=3; + gbc_clearLogButton.gridy=3; + gbc_clearLogButton.fill=GridBagConstraints.HORIZONTAL; + this.add(clearLogButton,gbc_clearLogButton); + } + + /** + * Set the correct text for all graphical elements according to the default {@link Locale}. + * @see java.util.Locale#getDefault() + */ + public void setText() { + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + clearLogButton.setText(resourceBundle.getString("clear")); + } + + /** + * Append data to the log. + * @param data message to append. + * @param error indicate if data correspond to an error or not. + */ + public void addLog(String data,Boolean error) { + try { + if(logStyleDocument.getLength()>0) data="\n"+data; + if(error) { + logStyleDocument.insertString(logStyleDocument.getLength(), data, errorAttributeSet); + }else { + logStyleDocument.insertString(logStyleDocument.getLength(), data, normalAttributeSet); + } + }catch(BadLocationException e) { + logTextPane.setText(logTextPane.getText()+"\n"+data.trim().trim()); + } + } +} diff --git a/client/src/main/Primary.java b/client/src/main/Primary.java new file mode 100644 index 0000000..d83ef5c --- /dev/null +++ b/client/src/main/Primary.java @@ -0,0 +1,76 @@ +package main; + +import java.util.Locale; +import java.util.ResourceBundle; + +import generic.Status; +import gui.GUI; +import network.Client; +import network.CommunicationManager; + +public class Primary { + + public static void main(String[] args) { + String serverAddress="127.0.0.1"; + int serverPort=6200; + String command=""; + + //Handle user arguments + if(args.length>0){ + for(int i=0;i connectStatus=communicationManager.connect(); + if(connectStatus.success==false) { + connectStatus.error.toString(); + return; + } + System.out.println(resourceBundle.getString("connectedTo")+": "+serverAddress+":"+serverPort); + + Status sendStatus=communicationManager.encryptAndSendString(command); + if(sendStatus.success) { + Status receiveStatus=communicationManager.receiveAndDecryptString(); + if(receiveStatus.success) { + System.out.println(receiveStatus.payload.trim()); + } else { + receiveStatus.error.toString(); + } + } else { + sendStatus.error.toString(); + } + communicationManager.disconnect(); + System.out.println(resourceBundle.getString("connectionEnded")); + } + +} diff --git a/client/src/network/Client.java b/client/src/network/Client.java new file mode 100644 index 0000000..efd9652 --- /dev/null +++ b/client/src/network/Client.java @@ -0,0 +1,159 @@ +package network; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.PrintStream; +import java.net.Socket; + +import generic.Status; +/** + * TCP client allowing to send and receive Objects. + */ +public class Client { + private int serverPort; + private String serverAddress; + private Socket socket; + private BufferedReader is; + private PrintStream os; + private ObjectInputStream ois; + private ObjectOutputStream oos; + + public Client(){ + serverPort=8033; + serverAddress="127.0.0.1"; + } + public Client(int port){ + serverPort=port; + serverAddress="127.0.0.1"; + } + public Client(int port,String ip){ + serverPort=port; + serverAddress=ip; + } + + public int getServerPort() { + return serverPort; + } + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + public String getServerAddress() { + return serverAddress; + } + public void setServerAddress(String serverAddress) { + this.serverAddress = serverAddress; + } + public Socket getSocket() { + return socket; + } + public void setSocket(Socket socket) { + this.socket = socket; + if(socket!=null) { + try { + setIs(new BufferedReader(new InputStreamReader(socket.getInputStream()))); + setOs(new PrintStream(socket.getOutputStream())); + setOos(new ObjectOutputStream(socket.getOutputStream())); + setOis(new ObjectInputStream(socket.getInputStream())); + } catch (Exception e) { + } + } + } + public BufferedReader getIs() { + return is; + } + public void setIs(BufferedReader is) { + this.is = is; + } + public PrintStream getOs() { + return os; + } + public void setOs(PrintStream os) { + this.os = os; + } + public ObjectInputStream getOis() { + return ois; + } + public void setOis(ObjectInputStream ois) { + this.ois = ois; + } + public ObjectOutputStream getOos() { + return oos; + } + public void setOos(ObjectOutputStream oos) { + this.oos = oos; + } + + public Status connect(){ + Status result=new Status(true); + try { + socket=new Socket(serverAddress,serverPort); + is=new BufferedReader(new InputStreamReader(socket.getInputStream())); + os=new PrintStream(socket.getOutputStream()); + ois=new ObjectInputStream(socket.getInputStream()); + oos=new ObjectOutputStream(socket.getOutputStream()); + result.message=socket.getInetAddress().toString().substring(1)+":"+socket.getPort(); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status sendString(String message){ + Status result=new Status(true); + try { + os.println(message); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status receiveString(){ + Status result=new Status(true); + try { + result.payload=is.readLine(); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status sendObject(Object object){ + Status result=new Status(true); + try { + oos.writeObject(object); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status receiveObject(){ + Status result=new Status(true); + try { + result.payload=ois.readObject(); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status close(){ + Status result=new Status(true); + if(this.socket==null) return result; + try { + socket.close(); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } +} \ No newline at end of file diff --git a/client/src/network/CommunicationManager.java b/client/src/network/CommunicationManager.java new file mode 100644 index 0000000..bd8cd7d --- /dev/null +++ b/client/src/network/CommunicationManager.java @@ -0,0 +1,340 @@ +package network; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import generic.Status; + +public class CommunicationManager { + private Client client; + /** Key used to initialize the connection. */ + private SecretKey masterKey; + /** Communication key between the client and the server. */ + private SecretKey communicationKey; + /** If true then the message will be send using the communicationKey. Otherwise, the masterKey will be used.*/ + private boolean useCommunicationKey; + + public CommunicationManager(Client client) { + this.client=client; + this.masterKey=new SecretKeySpec(new byte[]{-70,-45,-79,-32,-36,108,-117,-7,-97,56,46,-86,-35,-11,-35,-81}, "AES"); + this.useCommunicationKey=false; + } + + public String getAddress() { + if(client!=null) return client.getServerAddress(); + return ""; + } + public int getPort() { + if(client!=null) return client.getServerPort(); + return 0; + } + + /** + * Establish connection with the server set in the {@link #client}. + * If no error, then swap the key from master to communication. + * @return response {@link Status} object. + */ + public Status connect() { + Status status=new Status(true); + + //Stop immediately if already connected + if(client!=null && client.getSocket()!=null && client.getSocket().isConnected()) { + return status; + } + + //Connect to server + status=client.connect(); + if(status.success==false) { + return status; + } + + //Change from master key to a communication key + Status keyStatus=client.receiveString(); + if(keyStatus.success==false) { + status.success=keyStatus.success; + status.error=keyStatus.error; + return status; + } + Status decryptStatus=decryptString(keyStatus.payload, masterKey); + if(decryptStatus.success==false) { + status.success=decryptStatus.success; + status.error=decryptStatus.error; + return status; + } + String[] encodedKeyString=decryptStatus.payload.split(";"); + byte[] encodedKey=new byte[encodedKeyString.length]; + for(int i=0;i connect(String address,int port) { + if(client.getSocket()!=null && client.getSocket().isConnected()) { + disconnect(); + } + this.client=new Client(port, address); + return connect(); + } + + /** + * Encrypt and send data. + * @param data data to send. + * @return response {@link Status} object. + */ + public Status encryptAndSendString(String data) { + String encryptedMessage=""; + if(useCommunicationKey) { + encryptedMessage=encryptString(data, communicationKey).payload; + }else { + encryptedMessage=encryptString(data, masterKey).payload; + } + return client.sendString(encryptedMessage); + } + + /** + * Receive and decrypt data. + * @return response {@link Status} object. + */ + public Status receiveAndDecryptString() { + Status status=client.receiveString(); + if(status.success==false) { + status.success=status.success; + status.error=status.error; + return status; + } + + if(useCommunicationKey) { + status=decryptString(status.payload, communicationKey); + }else { + status=decryptString(status.payload, masterKey); + } + + if(status.success==false) { + status.success=status.success; + status.error=status.error; + } + return status; + } + + /** + * Close the communication socket. + * Inform the remote client of the deconnection. + * @return response {@link Status} object. + */ + public Status disconnect() { + encryptAndSendString("STOPstopSTOP"); + return client.close(); + } + + /** + * Generate and setup a communication key to replace the master key for future communication. + * Send the new key to the client. + */ + public void setupCommunicationKey() { + communicationKey=generateSecretKey(); + String encodedKey=""; + for(int i=0;i encrypt(byte[] data) { + if(useCommunicationKey) { + return encrypt(data,communicationKey); + }else{ + return encrypt(data,masterKey); + } + } + + /** + * Encrypt a message with the specified key. + * @param data message to encrypt. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status encrypt(byte[] data,SecretKey secretKey) { + Status result=new Status(true); + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encrypt=cipher.doFinal(data); + /* + * Base64.getEncoder().encode() is here to + * prevent javax.crypto.IllegalBlockSizeException: + * Input length must be multiple of 16 when decrypting with padded cipher + */ + result.payload=Base64.getEncoder().encode(encrypt); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + /** + * Encrypt a message. + * @param data message to encrypt. + * @return response {@link Status} object. + */ + public Status encryptString(String data) { + if(useCommunicationKey) { + return encryptString(data,communicationKey); + }else{ + return encryptString(data,masterKey); + } + } + + /** + * Use {@link #encrypt(String, SecretKey)} to encrypt a message. + * @param data message to encrypt. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status encryptString(String data,SecretKey secretKey) { + Status result=new Status(true); + Status encryptResult=encrypt(data.getBytes(StandardCharsets.UTF_8),secretKey); + + result.message=encryptResult.message; + result.success=encryptResult.success; + result.error=encryptResult.error; + if(encryptResult.success) { + result.payload=new String(encryptResult.payload,StandardCharsets.UTF_8); + } + return result; + } + + /*private String padStringForCrypto (String data) { + //Prevent javax.crypto.IllegalBlockSizeException: + //Input length must be multiple of 16 when decrypting with padded cipher + if(data.length()%16!=0) { + System.out.println("data length="+data.length()); + double dataLengthNeeded=Math.ceil((double)data.length()/16d)*16; + data=String.format("%-"+(int)dataLengthNeeded+"s", data); + System.out.println("data length="+data.length()); + } + return data; + }*/ + + /** + * Decrypt a message. + * @param data encrypted message. + * @return response {@link Status} object. + */ + public Status decrypt(byte[] data){ + if(useCommunicationKey) { + return decrypt(data,communicationKey); + }else{ + return decrypt(data,masterKey); + } + } + + /** + * Decrypt a message with the specified key. + * @param data encrypted message. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status decrypt(byte[] data,SecretKey secretKey) { + Status result=new Status(true); + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + /* + * Base64.getDecoder().decode() is here to + * prevent javax.crypto.IllegalBlockSizeException: + * Input length must be multiple of 16 when decrypting with padded cipher + */ + result.payload=cipher.doFinal(Base64.getDecoder().decode(data)); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + /** + * Decrypt a {@link String}. + * @param data encrypted message. + * @return response {@link Status} object. + */ + public Status decryptString(String data) { + if(useCommunicationKey) { + return decryptString(data,communicationKey); + }else{ + return decryptString(data,masterKey); + } + } + + /** + * Use {@link #encrypt(String, SecretKey)} to decrypt a message. + * @param data encrypted message. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status decryptString(String data,SecretKey secretKey) { + Status result=new Status(true); + + byte[] dataByte; + try { + dataByte=data.getBytes(StandardCharsets.UTF_8); + } catch (NullPointerException e) { + result.error=e; + result.success=false; + return result; + } + + Status decryptResult=decrypt(dataByte,secretKey); + result.message=decryptResult.message; + result.success=decryptResult.success; + result.error=decryptResult.error; + if(decryptResult.success) { + result.payload=new String(decryptResult.payload,StandardCharsets.UTF_8); + } + + return result; + } +} diff --git a/img/CommandPC_commandIpconfig.png b/img/CommandPC_commandIpconfig.png new file mode 100755 index 0000000..8201c43 Binary files /dev/null and b/img/CommandPC_commandIpconfig.png differ diff --git a/img/CommandPC_commandLs.png b/img/CommandPC_commandLs.png new file mode 100644 index 0000000..46598cd Binary files /dev/null and b/img/CommandPC_commandLs.png differ diff --git a/img/CommandPC_connectionResult.png b/img/CommandPC_connectionResult.png new file mode 100644 index 0000000..aecaa4e Binary files /dev/null and b/img/CommandPC_connectionResult.png differ diff --git a/img/CommandPC_systemTrayIcon.png b/img/CommandPC_systemTrayIcon.png new file mode 100644 index 0000000..39dbcfd Binary files /dev/null and b/img/CommandPC_systemTrayIcon.png differ diff --git a/img/CommandPC_systemTrayIcon2.png b/img/CommandPC_systemTrayIcon2.png new file mode 100644 index 0000000..298d1f3 Binary files /dev/null and b/img/CommandPC_systemTrayIcon2.png differ diff --git a/img/CommandPC_systemTrayLinux.png b/img/CommandPC_systemTrayLinux.png new file mode 100644 index 0000000..87cdf66 Binary files /dev/null and b/img/CommandPC_systemTrayLinux.png differ diff --git a/img/CommandPC_systemTrayWindows.png b/img/CommandPC_systemTrayWindows.png new file mode 100644 index 0000000..111f6c9 Binary files /dev/null and b/img/CommandPC_systemTrayWindows.png differ diff --git a/server/.classpath b/server/.classpath new file mode 100644 index 0000000..db07734 --- /dev/null +++ b/server/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..ed9b2c9 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,91 @@ +######################## +#JAVA +######################## +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +######################## +# ECLIPSE +######################## +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + diff --git a/server/.project b/server/.project new file mode 100644 index 0000000..33d3613 --- /dev/null +++ b/server/.project @@ -0,0 +1,17 @@ + + + server + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/server/resource/iconStart.png b/server/resource/iconStart.png new file mode 100644 index 0000000..39dbcfd Binary files /dev/null and b/server/resource/iconStart.png differ diff --git a/server/resource/iconStop.png b/server/resource/iconStop.png new file mode 100644 index 0000000..298d1f3 Binary files /dev/null and b/server/resource/iconStop.png differ diff --git a/server/src/bundle/Bundle.properties b/server/src/bundle/Bundle.properties new file mode 100644 index 0000000..43fa44f --- /dev/null +++ b/server/src/bundle/Bundle.properties @@ -0,0 +1,34 @@ + +clientConnection = Client connected + +clientConnectionFail = Client connection failed + +clientConnectionStop = Client disconnect + +help_argument = Arguments: + +help_help = Show this help message. + +help_intro = CommandPC is a program for executing commands on a remote machine regardless of the operating system. + +help_nogui = Don't start the graphical user interface. + +help_port = Set the server port to X. + +localPort = Local port + +message = Message + +messageFail = Error with the reception of the message + +quit = Quit + +serverEnd = Server stop + +serverEndAction = Stop the server + +serverStart = Server start + +serverStartAction = Start the server + +serverStartFail = Server start failed diff --git a/server/src/bundle/Bundle_fr.properties b/server/src/bundle/Bundle_fr.properties new file mode 100644 index 0000000..16c99e8 --- /dev/null +++ b/server/src/bundle/Bundle_fr.properties @@ -0,0 +1,34 @@ + +clientConnection = Client connect\u00E9 + +clientConnectionFail = \u00C9chec de connexion du client + +clientConnectionStop = D\u00E9connexion du client + +help_argument = Arguments: + +help_help = Affiche ce message d'aide. + +help_intro = CommandPC est un programme destin\u00E9 \u00E0 ex\u00E9cuter des commandes sur un serveur distant, quelque soit son syst\u00E8me d'exploitation. + +help_nogui = Ne d\u00E9mmare pas l'interface graphique. + +help_port = D\u00E9fini le port du serveur. + +localPort = Port local + +message = Message + +messageFail = Erreur lors de la r\u00E9ception du message + +quit = Arr\u00EAter + +serverEnd = Arr\u00EAt du serveur + +serverEndAction = Arr\u00EAter le serveur + +serverStart = D\u00E9marrage du serveur + +serverStartAction = D\u00E9marrer le serveur + +serverStartFail = \u00C9chec du d\u00E9marrage du serveur diff --git a/server/src/command/ExecuteCommand.java b/server/src/command/ExecuteCommand.java new file mode 100644 index 0000000..471f119 --- /dev/null +++ b/server/src/command/ExecuteCommand.java @@ -0,0 +1,57 @@ +package command; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Serializable; + +import generic.Status; + +/** + * Executes specified string command by the environment in which the application is running. + */ +public class ExecuteCommand implements Serializable{ + private static final long serialVersionUID = 1L; + private Runtime runtime; + private BufferedReader bufferReader; + + public ExecuteCommand(){ + runtime=Runtime.getRuntime(); + } + + public Runtime getRuntime() { + return runtime; + } + public void setRuntime(Runtime runtime) { + this.runtime = runtime; + } + + /** + * Executes the specified string command. + * @param command a specified system command. + * @return response {@link Status} object. + * @see Runtime#exec(String) + */ + public Status executeCommand(String command){ + Status result=new Status(false); + + String responseLine; + try { + Process execution=runtime.exec(command); + bufferReader=new BufferedReader(new InputStreamReader(execution.getInputStream())); + while ((responseLine = bufferReader.readLine()) != null){ + if(!(responseLine.isEmpty())){ + if(result.payload==null || result.payload.isBlank()) { + result.payload=responseLine; + }else { + result.payload=result.payload+"\n"+responseLine; + } + } + } + result.success=true; + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } +} \ No newline at end of file diff --git a/server/src/generic/Status.java b/server/src/generic/Status.java new file mode 100644 index 0000000..abfdaf0 --- /dev/null +++ b/server/src/generic/Status.java @@ -0,0 +1,53 @@ +package generic; + +/** + * Generic response class. + */ +public class Status { + /** Indicate if the operation was a success. By default, false. */ + public boolean success; + /** Response message.*/ + public String message; + /** Response payload.*/ + public T payload; + /** Response error */ + public E error; + + public Status() { + super(); + this.success=false; + } + public Status(boolean success) { + super(); + this.success = success; + } + public Status(boolean success, String message) { + super(); + this.success = success; + this.message = message; + } + public Status(boolean success, String message, T payload) { + super(); + this.success = success; + this.message = message; + this.payload = payload; + } + public Status(boolean success, String message, T payload, E error) { + super(); + this.success = success; + this.message = message; + this.payload = payload; + this.error = error; + } + @Override + public String toString() { + String result="Status [success="+success+", message="+message; + if(payload!=null) { + result+=", payload="+payload.toString(); + } + if(error!=null) { + result+=", error="+error.toString(); + } + return result+"]"; + } +} \ No newline at end of file diff --git a/server/src/gui/GUI.java b/server/src/gui/GUI.java new file mode 100644 index 0000000..a781202 --- /dev/null +++ b/server/src/gui/GUI.java @@ -0,0 +1,140 @@ +package gui; + +import java.awt.AWTException; +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.event.ActionEvent; +import java.net.InetAddress; +import java.util.Locale; +import java.util.ResourceBundle; + +import thread.ServerThread; + +public class GUI { + private TrayIcon trayIcon; + private Image iconServerStart; + private Image iconServerStop; + private PopupMenu trayPopupMenu; + private MenuItem trayPopupItemQuit; + private MenuItem trayPopupItemToggleServerState; + private boolean serverStarted; + + private ServerThread serverThread; + + public GUI(ServerThread serverThread) { + serverStarted=false; + this.serverThread=serverThread; + initialize(); + } + + public void show() { + try { + SystemTray.getSystemTray().add(trayIcon); + } catch (AWTException e) { + e.printStackTrace(); + } + } + + public void hide() { + SystemTray.getSystemTray().remove(trayIcon); + } + + private void initialize() { + if(SystemTray.isSupported()==false) { + return; + } + + //Setup TrayIcon image + iconServerStart=Toolkit.getDefaultToolkit().getImage(getClass().getResource("/iconStart.png")); + iconServerStop=Toolkit.getDefaultToolkit().getImage(getClass().getResource("/iconStop.png")); + + //Setup popup menu + trayPopupItemQuit=new MenuItem("trayPopupItemQuit"); + trayPopupItemQuit.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + serverThread.stopServerSocket(); + serverThread.stopClientsSocket(); + hide(); + } + }); + trayPopupItemToggleServerState=new MenuItem("trayPopupItemToggleServerState"); + trayPopupItemToggleServerState.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if(serverStarted) { + serverThread.stopServerSocket(); + serverThread.stopClientsSocket(); + } else { + if(serverThread.isAlive()==false) { + serverThread=new ServerThread(serverThread.getServer(), serverThread.getGui()); + serverThread.start(); + } + } + } + }); + trayPopupMenu=new PopupMenu(); + trayPopupMenu.add(trayPopupItemToggleServerState); + trayPopupMenu.addSeparator(); + trayPopupMenu.add(trayPopupItemQuit); + + trayIcon=new TrayIcon(iconServerStop,"CommandPC",trayPopupMenu); + trayIcon.setImageAutoSize(true); + + setText(); + } + + /** + * Set the correct text for all graphical elements according to the default {@link Locale}. + * @see java.util.Locale#getDefault() + */ + private void setText() { + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + trayPopupItemQuit.setLabel(resourceBundle.getString("quit")); + setTextServerState(); + } + + /** + * Set the correct text related to server state. + */ + private void setTextServerState() { + ResourceBundle resourceBundle=ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()); + if(serverStarted) { + trayPopupItemToggleServerState.setLabel(resourceBundle.getString("serverEndAction")); + } else { + trayPopupItemToggleServerState.setLabel(resourceBundle.getString("serverStartAction")); + } + } + + /** + * Indicate to the user that the server has started. + */ + public void displayServerStart() { + serverStarted=true; + trayIcon.setImage(iconServerStart); + setTextServerState(); + } + + /** + * Indicate to the user that the server has stop. + */ + public void displayServerStop() { + serverStarted=false; + trayIcon.setImage(iconServerStop); + setTextServerState(); + } + + /** + * Indicate to the user that a new client is connected. + * @param inetAddress client address + * @param port client port + * @param localPort local port + */ + public void displayStartConnection(InetAddress inetAddress, int port, int localPort) { + // TODO displayStartConnection() + } +} diff --git a/server/src/main/Primary.java b/server/src/main/Primary.java new file mode 100644 index 0000000..110e73d --- /dev/null +++ b/server/src/main/Primary.java @@ -0,0 +1,48 @@ +package main; + +import java.util.Locale; +import java.util.ResourceBundle; + +import gui.GUI; +import network.Server; +import thread.ServerThread; + +public class Primary { + + public static void main(String[] args) { + int port=6200; + boolean showGUI=true; + + //Handle user arguments + if(args.length>0){ + for(int i=0;i connect(){ + Status result=new Status(true); + try { + socket=new Socket(serverAddress,serverPort); + is=new BufferedReader(new InputStreamReader(socket.getInputStream())); + os=new PrintStream(socket.getOutputStream()); + ois=new ObjectInputStream(socket.getInputStream()); + oos=new ObjectOutputStream(socket.getOutputStream()); + result.message=socket.getInetAddress().toString().substring(1)+":"+socket.getPort(); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status sendString(String message){ + Status result=new Status(true); + try { + os.println(message); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status receiveString(){ + Status result=new Status(true); + try { + result.payload=is.readLine(); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status sendObject(Object object){ + Status result=new Status(true); + try { + oos.writeObject(object); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status receiveObject(){ + Status result=new Status(true); + try { + result.payload=ois.readObject(); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + public Status close(){ + Status result=new Status(true); + if(this.socket==null) return result; + try { + socket.close(); + } catch (IOException e) { + result.success=false; + result.error=e; + } + return result; + } +} \ No newline at end of file diff --git a/server/src/network/CommunicationManager.java b/server/src/network/CommunicationManager.java new file mode 100644 index 0000000..4ac3003 --- /dev/null +++ b/server/src/network/CommunicationManager.java @@ -0,0 +1,333 @@ +package network; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import generic.Status; + +public class CommunicationManager { + private Client client; + /** Key used to initialize the connection. */ + private SecretKey masterKey; + /** Communication key between the client and the server. */ + private SecretKey communicationKey; + /** If true then the message will be send using the communicationKey. Otherwise, the masterKey will be used.*/ + private boolean useCommunicationKey; + + public CommunicationManager(Client client) { + this.client=client; + this.masterKey=new SecretKeySpec(new byte[]{-70,-45,-79,-32,-36,108,-117,-7,-97,56,46,-86,-35,-11,-35,-81}, "AES"); + this.useCommunicationKey=false; + } + + public String getAddress() { + if(client!=null) return client.getServerAddress(); + return ""; + } + public int getPort() { + if(client!=null) return client.getServerPort(); + return 0; + } + + /** + * Establish connection with the server set in the {@link #client}. + * If no error, then swap the key from master to communication. + * @return response {@link Status} object. + */ + public Status connect() { + Status status=new Status(true); + + //Stop immediately if already connected + if(client!=null && client.getSocket()!=null && client.getSocket().isConnected()) { + return status; + } + + //Connect to server + status=client.connect(); + if(status.success==false) { + return status; + } + + //Change from master key to a communication key + Status keyStatus=client.receiveString(); + if(keyStatus.success==false) { + status.success=keyStatus.success; + status.error=keyStatus.error; + return status; + } + Status decryptStatus=decryptString(keyStatus.payload, masterKey); + if(decryptStatus.success==false) { + status.success=decryptStatus.success; + status.error=decryptStatus.error; + return status; + } + String[] encodedKeyString=decryptStatus.payload.split(";"); + byte[] encodedKey=new byte[encodedKeyString.length]; + for(int i=0;i connect(String address,int port) { + if(client.getSocket()!=null && client.getSocket().isConnected()) { + disconnect(); + } + this.client=new Client(port, address); + return connect(); + } + + /** + * Encrypt and send data. + * @param data data to send. + * @return response {@link Status} object. + */ + public Status encryptAndSendString(String data) { + String encryptedMessage=""; + if(useCommunicationKey) { + encryptedMessage=encryptString(data, communicationKey).payload; + }else { + encryptedMessage=encryptString(data, masterKey).payload; + } + return client.sendString(encryptedMessage); + } + + /** + * Receive and decrypt data. + * @return response {@link Status} object. + */ + public Status receiveAndDecryptString() { + Status status=client.receiveString(); + if(status.success==false) { + status.success=status.success; + status.error=status.error; + return status; + } + + if(useCommunicationKey) { + status=decryptString(status.payload, communicationKey); + }else { + status=decryptString(status.payload, masterKey); + } + + if(status.success==false) { + status.success=status.success; + status.error=status.error; + } + return status; + } + + /** + * Close the communication socket. + * Inform the remote client of the deconnection. + * @return response {@link Status} object. + */ + public Status disconnect() { + encryptAndSendString("STOPstopSTOP"); + return client.close(); + } + + /** + * Generate and setup a communication key to replace the master key for future communication. + * Send the new key to the client. + */ + public void setupCommunicationKey() { + communicationKey=generateSecretKey(); + String encodedKey=""; + for(int i=0;i encrypt(byte[] data) { + if(useCommunicationKey) { + return encrypt(data,communicationKey); + }else{ + return encrypt(data,masterKey); + } + } + + /** + * Encrypt a message with the specified key. + * @param data message to encrypt. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status encrypt(byte[] data,SecretKey secretKey) { + Status result=new Status(true); + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encrypt=cipher.doFinal(data); + /* + * Base64.getEncoder().encode() is here to + * prevent javax.crypto.IllegalBlockSizeException: + * Input length must be multiple of 16 when decrypting with padded cipher + */ + result.payload=Base64.getEncoder().encode(encrypt); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + /** + * Encrypt a message. + * @param data message to encrypt. + * @return response {@link Status} object. + */ + public Status encryptString(String data) { + if(useCommunicationKey) { + return encryptString(data,communicationKey); + }else{ + return encryptString(data,masterKey); + } + } + + /** + * Use {@link #encrypt(String, SecretKey)} to encrypt a message. + * @param data message to encrypt. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status encryptString(String data,SecretKey secretKey) { + Status result=new Status(true); + Status encryptResult=encrypt(data.getBytes(StandardCharsets.UTF_8),secretKey); + + result.message=encryptResult.message; + result.success=encryptResult.success; + result.error=encryptResult.error; + if(encryptResult.success) { + result.payload=new String(encryptResult.payload,StandardCharsets.UTF_8); + } + return result; + } + + /*private String padStringForCrypto (String data) { + //Prevent javax.crypto.IllegalBlockSizeException: + //Input length must be multiple of 16 when decrypting with padded cipher + if(data.length()%16!=0) { + System.out.println("data length="+data.length()); + double dataLengthNeeded=Math.ceil((double)data.length()/16d)*16; + data=String.format("%-"+(int)dataLengthNeeded+"s", data); + System.out.println("data length="+data.length()); + } + return data; + }*/ + + /** + * Decrypt a message. + * @param data encrypted message. + * @return response {@link Status} object. + */ + public Status decrypt(byte[] data){ + if(useCommunicationKey) { + return decrypt(data,communicationKey); + }else{ + return decrypt(data,masterKey); + } + } + + /** + * Decrypt a message with the specified key. + * @param data encrypted message. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status decrypt(byte[] data,SecretKey secretKey) { + Status result=new Status(true); + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + /* + * Base64.getDecoder().decode() is here to + * prevent javax.crypto.IllegalBlockSizeException: + * Input length must be multiple of 16 when decrypting with padded cipher + */ + result.payload=cipher.doFinal(Base64.getDecoder().decode(data)); + } catch (Exception e) { + result.success=false; + result.error=e; + } + return result; + } + + /** + * Decrypt a {@link String}. + * @param data encrypted message. + * @return response {@link Status} object. + */ + public Status decryptString(String data) { + if(useCommunicationKey) { + return decryptString(data,communicationKey); + }else{ + return decryptString(data,masterKey); + } + } + + /** + * Use {@link #encrypt(String, SecretKey)} to decrypt a message. + * @param data encrypted message. + * @param secretKey secret key to use. + * @return response {@link Status} object. + */ + private Status decryptString(String data,SecretKey secretKey) { + Status result=new Status(true); + + byte[] dataByte=data.getBytes(StandardCharsets.UTF_8); + + Status decryptResult=decrypt(dataByte,secretKey); + result.message=decryptResult.message; + result.success=decryptResult.success; + result.error=decryptResult.error; + if(decryptResult.success) { + result.payload=new String(decryptResult.payload,StandardCharsets.UTF_8); + } + + return result; + } +} diff --git a/server/src/network/Server.java b/server/src/network/Server.java new file mode 100644 index 0000000..299be30 --- /dev/null +++ b/server/src/network/Server.java @@ -0,0 +1,57 @@ +package network; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +import generic.Status; +/** + * TCP server allowing to send and receive Objects. + */ +public class Server { + private int serverPort; + private ServerSocket serverSocket; + private List clientSocket; + + public Server(int serverPort) { + super(); + this.serverPort = serverPort; + clientSocket=new ArrayList(); + } + + public Status start() { + Status result=new Status(true); + try { + serverSocket=new ServerSocket(serverPort); + result.payload=serverSocket.getLocalPort(); + } catch (IOException e) { + result.success=false; + result.error=e; + }; + return result; + } + + public Status accept() { + Status result=new Status(true); + try { + Socket client=serverSocket.accept(); + clientSocket.add(client); + result.payload=client; + } catch (IOException e) { + result.success=false; + result.error=e; + }; + return result; + } + + public void stop() { + try { + serverSocket.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/server/src/thread/ClientThread.java b/server/src/thread/ClientThread.java new file mode 100644 index 0000000..a5421b2 --- /dev/null +++ b/server/src/thread/ClientThread.java @@ -0,0 +1,98 @@ +package thread; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.Locale; +import java.util.ResourceBundle; + +import command.ExecuteCommand; +import generic.Status; +import network.Client; +import network.CommunicationManager; + +/** + * Thread dedicated to handling a communication with a client. + */ +public class ClientThread extends Thread{ + private CommunicationManager communicationManager; + private Client client; + private ExecuteCommand executeCommand; + private boolean run; + private boolean osIsWindows; + + public boolean isRun() { + return run; + } + public void setRun(boolean run) { + this.run = run; + } + + public ClientThread(Socket socket) { + super(); + this.client=new Client(); + this.client.setSocket(socket); + this.communicationManager=new CommunicationManager(client); + this.executeCommand=new ExecuteCommand(); + this.run=true; + this.osIsWindows=System.getProperty("os.name").toLowerCase().startsWith("windows"); + } + + private void closeConnection() { + communicationManager.encryptAndSendString("STOPstopSTOP"); + client.close(); + run=false; + } + + @Override + public void run() { + communicationManager.setupCommunicationKey(); + + Status status; + while(run) { + status=client.receiveString(); + if(status.success && status.payload==null && status.error==null) { + //Client is probably disconnected and client.receiveString() is just "purging" buffer + closeConnection(); + } + + if(run && status.success) { + status=communicationManager.decryptString(status.payload); + } + + if(run) { + if(status.success) { + if(status.payload.compareTo("STOPstopSTOP")==0) { + closeConnection(); + }else { + System.out.println( + ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("message") + +": "+status.payload + ); + + Status resultCommand=executeCommand.executeCommand(status.payload); + if(resultCommand.success) { + if(osIsWindows) { + try { + resultCommand.payload=new String(resultCommand.payload.getBytes(),"Cp850"); + } catch (UnsupportedEncodingException e) { + //If encoding fail do nothing as the text will still be mostly legible + } + } + communicationManager.encryptAndSendString(resultCommand.payload); + }else{ + communicationManager.encryptAndSendString(resultCommand.error.toString()); + } + } + }else { + System.out.println( + ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("messageFail") + +": "+status.error.toString() + ); + status.error.printStackTrace(); + } + } + } + System.out.println(ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("clientConnectionStop")); + } +} \ No newline at end of file diff --git a/server/src/thread/ServerThread.java b/server/src/thread/ServerThread.java new file mode 100644 index 0000000..668dd3e --- /dev/null +++ b/server/src/thread/ServerThread.java @@ -0,0 +1,114 @@ +package thread; + +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +import generic.Status; +import gui.GUI; +import network.Server; + +/** + * Thread dedicated to create {@link ClientThread} upon client connection. + */ +public class ServerThread extends Thread{ + private Server server; + private Boolean accept; + private GUI gui; + private List clients; + + public Server getServer() { + return server; + } + public void setServer(Server server) { + this.server = server; + } + public GUI getGui() { + return gui; + } + public void setGui(GUI gui) { + this.gui = gui; + } + public Boolean getAccept() { + return accept; + } + public void setAccept(Boolean accept) { + this.accept = accept; + } + + public ServerThread(Server server){ + super(); + this.clients=new ArrayList(); + this.server=server; + this.accept=true; + this.gui=null; + } + public ServerThread(Server server,GUI gui){ + super(); + this.server=server; + this.accept=true; + this.gui=gui; + } + + @Override + public void run(){ + accept=true; + Status serverStartStatus=server.start(); + if(serverStartStatus.success) { + System.out.println(ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("serverStart")); + if(gui!=null) gui.displayServerStart(); + while(accept) { + Status connectionAttempt=server.accept(); + if(connectionAttempt.success) { + Socket client=connectionAttempt.payload; + System.out.println( + ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("clientConnection") + +" "+client.getInetAddress()+":"+client.getPort() + +" (" + +ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("localPort") + +": "+client.getLocalPort() + +")" + ); + if(gui!=null) gui.displayStartConnection(client.getInetAddress(),client.getPort(),client.getLocalPort()); + ClientThread ct=new ClientThread(client); + clients.add(ct); + ct.start(); + }else { + if(accept) { + System.out.println(ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("clientConnectionFail")); + } + } + } + }else { + System.out.println(ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("serverStartFail")); + System.out.println(serverStartStatus.error.toString()); + return; + } + if(gui!=null) gui.displayServerStop(); + System.out.println(ResourceBundle.getBundle("bundle/Bundle",Locale.getDefault()).getString("serverEnd")); + } + + /** + * Stop the current server from accepting new connection. + * Old connection will remain active. + * @see #stopClientsSocket() + */ + public void stopServerSocket() { + server.stop(); + accept=false; + } + + /** + * Stop all the client connection. + * New connection are still possible. + * @see #stopServerSocket() + */ + public void stopClientsSocket() { + for(int i=0;i