Greetings.
Today, I would like to discuss the process of writing client-server applications that perform the functions of standard Windows utilities, such as Telnet, TFTP, and so on, in pure Java. It's clear that I'm not introducing anything new—these utilities have been working successfully for many years—but I believe not everyone is aware of what happens under the hood.
This is what we will talk about below.
In this article, to keep it concise, I will write only about the Telnet server, but I also have material on other utilities that will be covered in future installments of the series.
First of all, we should clarify what Telnet is, what it is used for, and how it works. I won’t quote sources verbatim (if needed, I will attach a link to the relevant materials at the end of the article), but I will say that Telnet provides remote access to a device's command line. Essentially, that is the extent of its functionality (I deliberately left out the server port connection, which will be discussed later). Therefore, to implement this, we need to accept a string on the client, send it to the server, attempt to pass it into the command line, read the command line response if there is one, send it back to the client, and display it on the screen, or, in case of an error, inform the user that something went wrong.
To implement what has been described above, we need 2 working classes and a test class from which we will launch the server and that the client will operate through.
Currently, the application structure includes:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Let's go through each of them:
TelnetClient
This class needs to be able to send the received commands and display the received responses. Furthermore, it should be capable of connecting to an arbitrary (as mentioned earlier) port of the remote device and disconnecting from it.
To accomplish this, the following functions have been implemented:
A function that accepts the socket address as an argument, opens the connection, and starts the input and output streams (the stream variables are declared above; the complete source code will be at the end of the article).
public void run(String ip, int port)
{
try {
Socket socket = new Socket(ip, port);
InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();
Scanner keyboard = new Scanner(System.in);
reader = new Thread(() -> read(keyboard, sout));
writer = new Thread(() -> write(sin));
reader.start();
writer.start();
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
An overloaded version of this function, connecting to the default port — for telnet, this is 23.
public void run(String ip)
{
run(ip, 23);
}
The function reads characters from the keyboard and sends them to the output socket — notably, in line mode rather than character mode:
private void read(Scanner keyboard, OutputStream sout)
{
try {
String input = new String();
while (true) {
input = keyboard.nextLine();
for (char i : (input + " n").toCharArray())
sout.write(i);
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
The function receives data from the socket and displays it on the screen.
private void write(InputStream sin)
{
try {
int tmp;
while (true){
tmp = sin.read();
System.out.print((char)tmp);
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
The function stops the reception and transmission of data.
public void stop()
{
reader.stop();
writer.stop();
}
}TelnetServer
This class should have the functionality to accept a command from the socket, send it for execution, and return the response back to the socket. The program intentionally lacks input validation because, firstly, even the 'boxed telnet' has the capability to format the server disk, and secondly, security issues are deliberately omitted in this article, which is why there is no mention of encryption or SSL.
There are only 2 functions (one of which is overloaded), and overall this is not a very good practice, but within the framework of this task, I found it appropriate to leave everything as it is.
boolean isRunning = true;
public void run(int port) {
(new Thread(()->{ try {
ServerSocket ss = new ServerSocket(port); // create a server socket and bind it to the specified port
System.out.println("Port " + port + " is waiting for connections");
Socket socket = ss.accept();
System.out.println("Connected");
System.out.println();
// Get the input and output streams of the socket, now we can receive and send data to the client.
InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();
Map env = System.getenv();
String wayToTemp = env.get("TEMP") + "tmp.txt";
for (int i :("Connectednnr".toCharArray()))
sout.write(i);
sout.flush();
String buffer = new String();
while (isRunning) {
int intReader = 0;
while ((char) intReader != 'n') {
intReader = sin.read();
buffer += (char) intReader;
}
final String inputToSubThread = "cmd /c " + buffer.substring(0, buffer.length()-2) + " 2>&1";
new Thread(() -> {
try {
Process p = Runtime.getRuntime().exec(inputToSubThread);
InputStream out = p.getInputStream();
Scanner fromProcess = new Scanner(out);
try {
while (fromProcess.hasNextLine()) {
String temp = fromProcess.nextLine();
System.out.println(temp);
for (char i : temp.toCharArray())
sout.write(i);
sout.write('n');
sout.write('r');
}
}
catch (Exception e) {
String output = "Something went wrong... Err code: " + e.getStackTrace();
System.out.println(output);
for (char i : output.toCharArray())
sout.write(i);
sout.write('n');
sout.write('r');
}
p.getErrorStream().close();
p.getOutputStream().close();
p.getInputStream().close();
sout.flush();
}
catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}).start();
System.out.println(buffer);
buffer = "";
}
}
catch(Exception x) {
System.out.println(x.getMessage());
}})).start();
}
The program opens a server port, reads data from it until it encounters the command termination symbol, sends the command to a new process, and the output from the process is redirected to the socket. It's as simple as a Kalashnikov.
Accordingly, there is an overload of this function with a default port:
public void run()
{
run(23);
}And correspondingly, the function that stops the server — also quite trivial, it breaks the infinite loop by violating its condition.
public void stop()
{
System.out.println("Server was stopped");
this.isRunning = false;
}I won't provide test classes here, they are available below — all they do is check the functionality of the public methods. Everything is on Git.
In summary, in a couple of evenings, you can understand the principles of the main console utilities. Now, when we telnet to a remote computer, we understand what is happening — the magic is gone.)
So, here are the links:
Source: habr.com
