Software de gravação com a funcionalidade dos utilitários cliente-servidor Windows, parte 01

Saudações.

Hoje eu gostaria de analisar o processo de criação de aplicativos cliente-servidor que executam as funções dos utilitários padrão do Windows, como Telnet, TFTP, etc., em Java puro. É claro que não trarei nada de novo - todos esses utilitários estão trabalhando com sucesso há mais de um ano, mas acredito que nem todo mundo sabe o que acontece sob o capô.

Isto é o que será discutido sob o corte.

Neste artigo, para não atrasá-lo, além de informações gerais, escreverei apenas sobre o servidor Telnet, mas no momento ainda há material em outros utilitários - ele estará nas partes posteriores do ciclo.

Primeiro de tudo, você deve entender o que é o Telnet, por que é necessário e com o que é consumido. Não citarei as fontes literalmente (se necessário, anexarei um link para materiais relacionados no final do artigo); só posso dizer que o Telnet fornece acesso remoto à linha de comando do dispositivo. Em geral, é aí que a funcionalidade termina (em silêncio, entrei em contato silenciosamente com a porta do servidor, mais adiante). Portanto, para sua implementação, precisamos aceitar a linha no cliente, transferi-la para o servidor, tentar transferi-la para a linha de comando, ler a resposta da linha de comando, se houver, transferi-la de volta para o cliente e exibi-la ou, em caso de ocorrência erros, faça o usuário entender que algo está errado.

Para implementar o que foi dito acima, você precisa de 2 classes de trabalho e alguma classe de teste a partir da qual iniciaremos o servidor e através da qual o cliente trabalhará.
Por conseguinte, no momento, a estrutura do aplicativo inclui:

  • Telnetclient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Vamos examinar cada um deles:

Telnetclient

Tudo o que essa classe deve poder fazer é enviar os comandos recebidos e mostrar as respostas recebidas. Além disso, você precisa conectar-se a uma porta arbitrária (como mencionada acima) do dispositivo remoto e desconectá-lo.

Para isso, foram implementadas as seguintes funções:

Uma função que usa um endereço de soquete como argumento, abre uma conexão e inicia fluxos de entrada e saída (variáveis ​​de fluxo são declaradas acima, fontes completas no final do artigo).

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()); } } 

Sobrecarregando a mesma função, conectando à porta padrão - para telnet isso é 23

 public void run(String ip) { run(ip, 23); } 

A função lê caracteres do teclado e os envia para o soquete de saída - o que é típico, na cadeia de caracteres, e não no modo de caractere:

 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()); } } 

A função recebe dados do soquete e os exibe na tela.
 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()); } } 

A função para de receber e transmitir dados.
 public void stop() { reader.stop(); writer.stop(); } } 

TelnetServer

Essa classe deve ter a funcionalidade de aceitar um comando de um soquete, enviando-o para execução e enviando uma resposta do comando de volta para o soquete. O programa não verifica intencionalmente os dados de entrada, porque, em primeiro lugar, existe a oportunidade de formatar o disco do servidor no "box telnet" e, em segundo lugar, o problema de segurança neste artigo é omitido em princípio, e é por isso que não há uma palavra sobre criptografia ou SSL

Existem apenas duas funções (uma delas está sobrecarregada) e, em geral, essa não é uma boa prática; no entanto, como parte dessa tarefa, me pareceu apropriado deixar tudo como está.

  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(); } 

O programa abre a porta do servidor, lê os dados a partir dele, até encontrar um caractere de encerramento de comando, transfere o comando para um novo processo e a saída do processo é redirecionada para o soquete. Tudo é como um rifle de assalto Kalashnikov.

Consequentemente, para esta função, há sobrecarga com a porta padrão:

  public void run() { run(23); } 

Bem, e, consequentemente, a função que para o servidor também é trivial, interrompe o ciclo eterno, violando sua condição.

  public void stop() { System.out.println("Server was stopped"); this.isRunning = false; } 

Não darei aulas de teste aqui, elas estão no final - tudo o que fazem é verificar o desempenho dos métodos públicos. Tudo está no gita.

Resumindo, em algumas noites você pode entender os princípios de operação dos principais utilitários do console. Agora, quando chegamos ao computador remoto, entendemos o que está acontecendo - a mágica desapareceu)

Portanto, os links são:
Todas as fontes foram, são e estarão aqui
Sobre o Telnet
Mais sobre o Telnet

Source: https://habr.com/ru/post/pt460569/


All Articles