Desenvolvimento de software com funcionalidades de utilitários cliente-servidor Windows, parte 01

Saudações.

Hoje, gostaríamos de discutir o processo de criação de aplicativos cliente-servidor que desempenham funções das utilidades padrão do Windows, como Telnet, TFTP, etc., em Java puro. É claro que não trarei nada de novo — todas essas utilidades já funcionam com sucesso há muitos anos, mas acredito que não todos conhecem o que acontece nos bastidores.

É exatamente sobre isso que vamos falar abaixo.

Neste artigo, para não torná-lo muito longo, além de informações gerais, falarei apenas sobre o servidor Telnet, mas atualmente há material sobre outras utilidades — isso será apresentado em partes futuras do ciclo.

Antes de tudo, precisamos entender o que é o Telnet, para que serve e como funciona. Não irei citar fontes literalmente (se necessário — no final do artigo incluirei links para materiais sobre o tópico), apenas direi que o Telnet fornece acesso remoto ao prompt de comando de um dispositivo. Em essência, essa é a sua funcionalidade principal (eu me omiti de mencionar o acesso à porta do servidor intencionalmente, falarei sobre isso mais tarde). Portanto, para a sua implementação, precisamos receber uma linha no cliente, transmiti-la para o servidor, tentar passar para o prompt de comando, ler a resposta do prompt de comando, se existir, enviá-la de volta ao cliente e exibi-la na tela ou, em caso de erro, informar ao usuário que algo está errado.

Para a implementação acima, precisamos, portanto, de 2 classes operacionais e de uma classe de teste a partir da qual iniciaremos o servidor e pela qual o cliente funcionará.
Atualmente, a estrutura do aplicativo inclui:

  • TelnetClient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Vamos dar uma olhada em cada um deles:

TelnetClient

Tudo que essa classe deve fazer — é enviar os comandos recebidos e mostrar as respostas recebidas. Além disso, deve ser capaz de se conectar a qualquer porta (como mencionei anteriormente) de um dispositivo remoto e desconectar-se dela.

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

Uma função que aceita como argumento o endereço do socket, abre a conexão e inicia os fluxos de entrada e saída (as variáveis do fluxo estão declaradas acima, o código completo está 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());
        }
    }

Sobrecarga da mesma função, conectando-se à porta padrão — para Telnet é 23


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

A função lê caracteres do teclado e os envia para o socket de saída — o que é característico, no modo de linha, 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 socket 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 interrompe a recepção e a transmissão de dados


    public void stop()
    {
        reader.stop();
        writer.stop();
    }
}

TelnetServer

Esta classe deve ter a funcionalidade de aceitar comandos do socket, enviá-los para execução e enviar a resposta do comando de volta ao socket. O programa não verifica intencionalmente a entrada, porque em primeiro lugar, no 'Telnet de caixa' há a possibilidade de formatar o disco do servidor, e em segundo lugar, a questão da segurança foi deixada de lado neste artigo, e é por isso que não há menção a criptografia ou SSL.

Aqui estão apenas 2 funções (uma delas sobrecarregada), e no geral não é uma prática muito boa, mas no contexto dessa tarefa, achei apropriado deixar tudo como está.

 boolean isRunning = true;
    public void run(int port)    {

        (new Thread(()->{ try {
            ServerSocket ss = new ServerSocket(port); // cria um socket de servidor e vincula-o à porta especificada
            System.out.println("Port " + port + " está aguardando conexões");

            Socket socket = ss.accept();
            System.out.println("Conectado");
            System.out.println();

            // Obtém os fluxos de entrada e saída do socket, agora podemos receber e enviar dados ao cliente.
            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 = "Algo deu errado... Código de erro: " + 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("Erro: " + e.getMessage());
                    }
                }).start();
                System.out.println(buffer);
                buffer = "";

            }
        }
        catch(Exception x) {
            System.out.println(x.getMessage());
        }})).start();

    }

O programa abre uma porta de servidor, lê os dados dela até encontrar o símbolo de término do comando, envia o comando para um novo processo, e a saída do processo é redirecionada para o socket. É tão simples quanto uma metralhadora Kalashnikov.

Consequentemente, existe uma sobrecarga para essa função com a porta padrão:

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

E, claro, a função que para o servidor também é trivial; ela interrompe o loop infinito, quebrando sua condição.

    public void stop()
    {
        System.out.println("O servidor foi parado");
        this.isRunning = false;
    }

Não vou apresentar classes de teste aqui, elas estão abaixo — tudo o que fazem é verificar a funcionalidade dos métodos públicos. Tudo está no Git.

Resumindo, em algumas noites, você pode entender os princípios de funcionamento das principais utilitários de console. Agora, quando fazemos Telnet para um computador remoto, entendemos o que está acontecendo — a mágica desapareceu)

Então, os links:
Todos os códigos-fonte estavam, estão e sempre estarão aqui
Sobre Telnet
Mais sobre Telnet

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster