Сегодня хотелось бы разобрать процесс написания клиент-серверных приложений, выполняющих функции стандартных утилит Windows, как то Telnet, TFTP, et cetera, et cetera на чистой Javа. Понятно, что ничего нового я не привнесу — все эти утилиты уже успешно работают не один год, но, полагаю, что происходит под капотом у них знают не все.
Именно об этом и пойдет речь под катом.
В этой статье, для того, что бы ее не затягивать, помимо общей информации я напишу только о Telnet сервере, но на данный момент есть еще материал по другим утилитам — он будет в дальнейших частях цикла.
Прежде всего, следует разобраться, что же такое Telnet, для чего он нужен и с чем его едят. Не буду дословно цитировать источники(если надо — в конце статьи прикреплю ссылку на материалы по теме), скажу лишь, что Telnet обеспечивает удаленный доступ к командной строке устройства. По большому, на этом его функционал заканчивается (о обращении к серверному порту умолчал осознанно, об этом позднее). Значит, для его реализации, нам нужно принять строку на клиенте, передать ее на сервер, попытаться передать ее в командную строку, считать ответ командной строки, если он есть, передать его обратно на клиент и вывести на экран, либо же, в случае возникновения ошибки, дать пользователю понять, что что-то не так.
Для реализации вышеизложенного, соответственно, нужно 2 рабочих класса, и некоторый тестовый класс, из которого мы будем запускать сервер и через который будет работать клиент.
Соответственно, на данный момент структура приложения включает в себя:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Пробежимся по каждому из них:
TelnetClient
Все, что должен уметь делать этот класс — отправлять полученные команды и показывать полученные ответы. Кроме того, нужно уметь подключаться к произвольному (о чем говорил выше) порту удаленного устройства и отключаться от него.
Для этого были реализованы следующие функции:
Функция, принимающая в качестве аргумента адрес сокета, открывающая соединение и запискающая потоки ввода и вывода (переменыые потока объявлены выше, полные исходники — в конце статьи)
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()); } }
Перегрузка этой же функции, поключающаяся к порту по умолчанию — для телнета это 23
public void run(String ip) { run(ip, 23); }
Функция читает символы с клавиатуры и отправляет их на выходной сокет — что характерно, в строчом, а не символьном режиме
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()); } }
Функция принимает данные с сокета и выводит их на экран
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()); } }
Функция останавливает прием и передачу данных
public void stop() { reader.stop(); writer.stop(); } }
TelnetServer
Этот класс должен обладать функционалом принятия команды с сокета, отправки ее на исполнение и отправки ответа от команды обратно на сокет. В программе умышленно нет проверки входных данных, потому что во-первых, и в «коробочном телнете» есть возможность форматнуть диск сервера, а во-вторых, вопрос безопасности в этой статье опущен в принципе, и именно поэтому тут нет ни слова о шифровании или SSL.
Тут всего 2 функции(одна из них перегружена), и в целом это не очень хорошая практика, однако в рамках данной задачи мне показалось уместным оставить все как есть.
boolean isRunning = true; public void run(int port) { (new Thread(()->{ try { ServerSocket ss = new ServerSocket(port); // создаем сокет сервера и привязываем его к вышеуказанному порту System.out.println("Port "+port+" is waiting for connections"); Socket socket = ss.accept(); System.out.println("Connected"); System.out.println(); // Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиенту. InputStream sin = socket.getInputStream(); OutputStream sout = socket.getOutputStream(); Map<String, String> env = System.getenv(); String wayToTemp = env.get("TEMP") + "\\tmp.txt"; for (int i :("Connected\n\n\r".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 gets 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(); }
Программа открывает серверный порт, читает с него данные, пока не встретит символ окончания команды, передает команду в новый процесс, а вывод из процесса перенаправлен в сокет. Все просто как автомат Калашникова.
Соответственно, для этой функции существует перегрузка с портом по умолчанию
public void run() { run(23); }
Ну и соответственно, функция, останавливающая сервер — тоже все тривиально, она прерывает вечный цикл, нарушая его условие.
public void stop() { System.out.println("Server was stopped"); this.isRunning = false; }
Тестовые классы я тут приводить не буду, они есть внизу — все что они делают — проверяют работоспособность публичных методов. Все есть на гите
Резюмируя, за пару вечеров можно понять принципы действия основных консольных утилит. Теперь, когда мы телнетимся к удаленному компу, мы понимаем, что происходит — магия исчезла)
Итак, ссылки:
github.com/Toxa-p07a1330/Temviewer — все исходники были, есть и будут есть здесь
www.extrahop.com/resources/protocols/telnet — о Телнете
www.lifewire.com/what-does-telnet-do-2483642 — еще о Телнете
ссылка на оригинал статьи https://habr.com/ru/post/460569/