Escrevendo software com funcionalidade de utilitários cliente-servidor do Windows, parte 01

Bem-vindo.

Hoje eu gostaria de examinar o processo de gravação de aplicativos cliente-servidor que executam as funções de utilitários padrão do Windows, como Telnet, TFTP, etc., etc., em Java puro. É claro que não trarei nada de novo - todos esses utilitários estão funcionando com sucesso há mais de um ano, mas acredito que nem todo mundo sabe o que está acontecendo nos bastidores.

Isso é exatamente o que será discutido no corte.

Neste artigo, para não me arrastar, além das informações gerais, escreverei apenas sobre o servidor Telnet, mas no momento também há material sobre outros utilitários - estará nas próximas partes da série.

Primeiro de tudo, você precisa descobrir o que é Telnet, para que é necessário e para que é usado. Não citarei as fontes literalmente (se necessário, anexarei um link para materiais sobre o tema no final do artigo), direi apenas que o Telnet fornece acesso remoto à linha de comando do dispositivo. Em geral, é aqui que termina sua funcionalidade (mantive silêncio deliberadamente sobre o acesso à porta do servidor; falaremos mais sobre isso mais tarde). Isso significa que para implementá-lo precisamos aceitar uma linha no cliente, passá-la para o servidor, tentar passá-la para a linha de comando, ler a resposta da linha de comando, se houver, passá-la de volta para o cliente e exibi-lo na tela ou, em caso de erros, informar ao usuário que algo está errado.

Para implementar o acima exposto, precisamos de 2 classes de trabalho e alguma classe de teste a partir da qual iniciaremos o servidor e através da qual o cliente funcionará.
Assim, no momento a estrutura do aplicativo inclui:

  • Cliente Telnet
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Vamos examinar cada um deles:

Cliente Telnet

Tudo o que esta classe deve ser capaz de fazer é enviar comandos recebidos e mostrar as respostas recebidas. Além disso, você precisa ser capaz de se conectar a uma porta arbitrária (como mencionado acima) de um dispositivo remoto e desconectar-se dele.

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

Uma função que recebe um endereço de soquete como argumento, abre uma conexão e inicia fluxos de entrada e saída (as variáveis ​​de fluxo são declaradas acima, as fontes completas estão 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-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 soquete de saída - o que é típico no modo de linha, 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 interrompe a recepção e transmissão de dados


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

TelnetServer

Esta classe deve ter a funcionalidade de receber um comando de um soquete, enviá-lo para execução e enviar uma resposta do comando de volta ao soquete. O programa deliberadamente não verifica os dados de entrada, porque em primeiro lugar, mesmo em “telnet in a box” é possível formatar o disco do servidor e, em segundo lugar, a questão da segurança neste artigo é omitida em princípio, e é por isso que não há uma palavra sobre criptografia ou SSL.

Existem apenas 2 funções (uma delas está sobrecarregada) e em geral esta não é uma prática muito boa, mas para os efeitos desta tarefa pareceu-me adequado 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 :("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 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 dela até encontrar um caractere de final de comando, passa o comando para um novo processo e redireciona a saída do processo para o soquete. Tudo é tão simples quanto um rifle de assalto Kalashnikov.

Conseqüentemente, há uma sobrecarga para esta função com uma porta padrão:

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

Pois bem, portanto, a função que para o servidor também é trivial, ela interrompe o loop eterno, violando sua condição.

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

Não vou dar aulas 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ê poderá entender os princípios de operação dos principais utilitários do console. Agora, quando nos telenetamos para um computador remoto, entendemos o que está acontecendo - a mágica desapareceu)

Então, os links:
Todas as fontes estiveram, estão e estarão aqui
Sobre Telnet
Mais sobre Telnet

Fonte: habr.com

Adicionar um comentário