Pisanie oprogramowania z funkcjonalnością klient-serwer w systemie Windows, część 01

Witam.

Dziś chciałbym omówić proces pisania aplikacji kliencko-serwerowych, które wykonują funkcje standardowych narzędzi systemu Windows, takich jak Telnet, TFTP i tak dalej, w czystej Javie. Oczywiście nie wnoszę nic nowego — wszystkie te narzędzia działają już od lat, ale myślę, że nie wszyscy wiedzą, co dzieje się pod powierzchnią.

Właśnie o tym będzie mowa w dalszej części artykułu.

W tym artykule, aby go nie przedłużać, oprócz ogólnych informacji opiszę tylko serwer Telnet, ale na ten moment mam też materiały o innych narzędziach — będzie to w kolejnych częściach cyklu.

Przede wszystkim warto wyjaśnić, czym właściwie jest Telnet, do czego służy i jak go używać. Nie będę dosłownie cytować źródeł (jeśli trzeba — na końcu artykułu dołączę linki do materiałów w temacie), powiem tylko, że Telnet zapewnia zdalny dostęp do wiersza poleceń urządzenia. W zasadzie to jego funkcjonalność się kończy (o zwracaniu do portu serwera nie wspomniałem świadomie, o tym później). Oznacza to, że do jego realizacji musimy odebrać ciąg na kliencie, przekazać go do serwera, spróbować przekazać go do wiersza poleceń, odczytać odpowiedź z wiersza poleceń, jeśli taka istnieje, przekazać ją z powrotem do klienta i wyświetlić na ekranie, lub w przypadku wystąpienia błędu dać użytkownikowi znać, że coś poszło nie tak.

Aby zrealizować powyższe, potrzebujemy odpowiednio dwóch klas roboczych oraz klasy testowej, z której uruchomimy serwer i przez którą będzie działał klient.
Obecnie struktura aplikacji obejmuje:

  • TelnetClient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Przejdźmy do każdej z nich:

TelnetClient

Wszystko, co ten klas musi umieć, to wysyłać otrzymane polecenia i pokazywać otrzymane odpowiedzi. Ponadto musi umieć łączyć się z dowolnym (o czym wspomniałem powyżej) portem zdalnego urządzenia i rozłączać się z nim.

W tym celu zaimplementowano następujące funkcje:

Funkcja, która przyjmuje jako argument adres gniazda, otwiera połączenie i uruchamia strumienie wejścia i wyjścia (zmienne strumieni są zadeklarowane powyżej, pełne źródła — na końcu artykułu).

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

Przeciążenie tej samej funkcji, które łączy się z domyślnym portem — dla telnetu to 23


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

Funkcja odczytuje znaki z klawiatury i przesyła je do wyjściowego gniazda — co jest charakterystyczne, w trybie liniowym, a nie znakowym:


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

Funkcja odbiera dane z gniazda i wyświetla je na ekranie


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

Funkcja zatrzymuje odbiór i przesyłanie danych


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

TelnetServer

Ta klasa powinna mieć funkcjonalność przyjmowania polecenia z gniazda, wysyłania go do wykonania i przesyłania odpowiedzi z powrotem do gniazda. W programie celowo nie ma sprawdzania danych wejściowych, ponieważ po pierwsze, w „pudełkowym telnecie” istnieje możliwość sformatowania dysku serwera, a po drugie, kwestia bezpieczeństwa w tym artykule jest pominięta, dlatego nie ma ani słowa o szyfrowaniu lub SSL.

Tutaj są tylko 2 funkcje (jedna z nich jest przeciążona), i ogólnie nie jest to najlepsza praktyka, jednak w ramach tego zadania uznałem, że warto zostawić wszystko, jak jest.

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

        (new Thread(()->{ try {
            ServerSocket ss = new ServerSocket(port); // tworzymy gniazdo serwera i przypisujemy je do podanego portu
            System.out.println("Port "+port+" oczekuje na połączenia");

            Socket socket = ss.accept();
            System.out.println("Połączono");
            System.out.println();

            // Pobieramy strumienie wejściowe i wyjściowe gniazda, teraz możemy odbierać i wysyłać dane do klienta.
            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 = "Coś poszło nie tak... Kod błędu: " + 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("Błąd: " + e.getMessage());
                    }
                }).start();
                System.out.println(buffer);
                buffer = "";

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

    }

Program otwiera port serwera, odczytuje z niego dane, aż natrafi na znak końca komendy, następnie przekazuje komendę do nowego procesu, a wyjście z procesu jest przekierowywane do gniazda. Proste jak automat kałasznikowa.

Odpowiednio, dla tej funkcji istnieje przeciążenie z domyślnym portem:

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

No i odpowiednio, funkcja zatrzymująca serwer — też wszystko trywialne, przerywa wieczną pętlę, naruszając jej warunek.

    public void stop()
    {
        System.out.println("Serwer został zatrzymany");
        this.isRunning = false;
    }

Nie będę tu przytaczać klas testowych, są na dole — wszystko, co robią, to sprawdzają działanie publicznych metod. Wszystko znajduje się na gicie.

Podsumowując, w ciągu kilku wieczorów można zrozumieć zasady działania podstawowych narzędzi konsolowych. Teraz, gdy połączymy się z zdalnym komputerem, rozumiemy, co się dzieje — magia zniknęła)

A zatem, linki:
Wszystkie źródła były, są i będą tutaj
O Telnecie
Jeszcze o Telnecie

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster