Java аплети и сигурност. Комуникация със сървъра

Светлин Наков

Сигурността на аплетите е важна тяхна черта. Никой потребител не би се съгласил да разглежда сайтове с аплети, ако те могат да пишат свободно по диска му, ако могат да откраднат негова лична информация, да изпращат email-и от негово име или да извършват някаква друга злонамерена дейност.

За решаването на този проблем аплетите са проектирани да работят с ограничени права. Сигурността в Java е част от самата платформа и се конфигурира от един специален файл с име java.policy. В зависимост от правата, които Web-браузърът иска да даде на аплета, се подготвя съответен файл, който ги описва и виртуалната машина се конфигурира по него.

В някои браузъри правата могат да се настройват и се допуска възможност потребителят да дава пълно доверие на определени сайтове, с което аплетите се освобождават от ограниченията си.

Съществува и друга стандартна възможност – аплетите да се подписват цифрово. Подписаните аплети могат да се изпълняват без ограничения на правата, но само ако при зареждането им потребителят им разреши това. Подписаните аплети доказват пред потребителя че са безопасни чрез цифров сертификат, използван при подписването им. Ако потребителят вярва на сертификата, той може да се съгласи да ги изпълни без ограничения на правата, а в противен случай, ако не им вярва, те се изпълняват както обикновени аплет.

Какви права имат аплетите

Ако потребителят не е посочил нещо друго, се използват стандартните настройки за правата на аплетите, които налагат следните ограничения: Аплетите не могат да четат и пишат по диска на машината, на която се изпълняват. Не могат да осъществяват достъп до чужда памет, дори в операционни системи, в които няма защита на паметта. Не могат да отварят сокет до произволен сървър в Интернет. Могат да отварят сокет само до хост-а, от който са заредени. Не могат да извикват директно native код. Не могат да предизвикат претоварване или забиване на машината, на която се изпълняват. На практика последното понякога е възможно да се случи в някои специфични ситуации, но това се дължи на грешки и пропуски в сигурността на съответните браузъри и виртуалните машини, които те използват.

Аплетите, сигурността и комуникацията със сокети

Трябва да обърнем специално внимание на сокетите. Свидетели сме на много аплети, които извършват активна мрежова дейност, като например аплети за разговори (chat), аплети за четене на e-mail, аплети за изпращане на e-mail, различни игри и др. Всички те използват сокет-базирана комуникация и изглежда, че отварят сокет към Интернет. Например при изпращането на поща аплетът комуникира със зададен от потребителя SMTP сървър. Това, обаче, не става директно, както при обикновените програми на Java.

Аплетите имат право да се свързват чрез сокет само до сървъра, от който са заредени, т.е. към хост-а върнат от метода getCodeBase().getHost(). Ето защо аплети, които не са заредени от някой Web-сървър, а локално от файловата система, чрез отваряне на локален HTML файл, нямат право да отварят никакви сокети. Това защитава потребителите от атака чрез HTML документи съдържащи аплети със злонамерено действие. Всички аплети, които изглежда, че отварят сокети към Интернет, всъщност отварят сокети към сървъра, от който са заредени и от получат пренасочване към заявения хост, т.е. използват Web-сървъра като прокси (междинен пренасочващ сървър).

Когато се наложи да пишем аплет, който комуникира чрез сокети, е необходимо на Web-сървъра, където се хоства този аплет да пуснем някакъв допълнителен сървър, който осигурява комуникацията на аплета с услугата, до която той трябва да осъществява достъп. Разбира се, това трябва да става след успешна автентикация на потребителя в системата. За целта най-удобно е сървърът, който се грижи за комуникацията на аплета да се интегрира в Web-сървъра, за да може да използва информацията от сесията на потребителя, който е изпълнил аплета.

Пример за аплет, който комуникира със сървъра

Ще дадем един пример, който илюстрира как можем да реализираме аплет, който асинхронно получава данни от сървъра. Да си представим, че искаме да направим сайт, от който да се следят цените на акциите на големи компании на борсата. Цената на акциите на борсата е нещо, което много бързо се променя (всяка секунда или дори по-често). Искаме постоянно да визуализираме актуалната в момента цена за дадена компания.

Да предположим, че имаме TCP сървър, който приема идентификатор на компания (company ticker), след което започва постоянно да изпраща информация за цената на акциите на тази компания при всяка нейна промяна. Искаме да направим аплет, който отваря един TCP сокет, изпраща по него към сървъра идентификатор на компания и след това постоянно отпечатва всичко, което прочете от сокета. Една примерна реализация може да изглежда по следния начин:

StockQuoteApplet.java
import java.applet.Applet; 
import java.awt.*; 
import java.net.Socket; 
import java.io.*; 
 
public class StockQuoteApplet extends Applet 
        implements Runnable { 
    public static final int STOCK_SERVER_PORT = 2004; 
    public static final String COMPANY_TICKER = "MSFT"; 
 
    private BufferedReader mSocketReader; 
    private TextArea mTextArea = new TextArea(); 
 
    public void init() { 
        try { 
            // Establish TCP socket connection with the server 
            String host = this.getCodeBase().getHost(); 
            Socket sock = new Socket(host, STOCK_SERVER_PORT); 
 
            // Send the company ticker to the server 
            OutputStreamWriter socketWriter = 
                new OutputStreamWriter(sock.getOutputStream()); 
            socketWriter.write(COMPANY_TICKER + "\n"); 
            socketWriter.flush(); 
 
            // Get the input stream reader 
            mSocketReader = new BufferedReader( 
                new InputStreamReader(sock.getInputStream())); 
        } catch (IOException ioex) { 
            ioex.printStackTrace(); 
            System.exit(-1); 
        } 
 
        // Set the layout manager to null 
        this.setLayout(null); 
 
        // Create the text area and add it to the applet 
        mTextArea.setBounds(new Rectangle(0, 0, 300, 150)); 
        this.add(mTextArea); 
 
        // Create and start socket reader thread 
        Thread sockerReaderThread = new Thread(this); 
        sockerReaderThread.start(); 
    } 
 
    public void run() { 
        try { 
            while (true) { 
                String line = mSocketReader.readLine(); 
                mTextArea.append(line); 
                mTextArea.append("\n"); 
            } 
        } catch (IOException ioex) { 
            ioex.printStackTrace(); 
        } 
    } 
}

Аплетът отваря сокет към сървъра по време на инициализацията си, след което изпраща по сокета идентификатор на компания (в случая MSFT) и стартира отделна нишка, която да чете постоянно данните идващи от сокета и да ги добавя в текстовата област. Нужда от синхронизация не е необходима, защото текстовата област се променя само от една нишка и не може да стане конфликт.

Изглед от аплета за визуализация на акциите на борсата

Ето как изглежда аплета в действие (в Internet Explorer 6):

Ето и примерен HTML код, с който може да се изпълни този аплет:

TestStockQuoteApplet.html
<html><body><center> 
<applet code="StockQuoteApplet.class" codebase="." 
    width="300" height="150"> 
</applet> 
</center></body></html>

Остава да предложим и примерна реализация на сървъра за следене на акциите на борсата. За леснота ще изпращаме на клиентите случайни данни с пълното съзнание, че са фалшиви. В реална ситуация данните биха могли да се извличат от някаква база данни или от друг сървър. Ето как би могъл да изглежда сорс-кодът:

StockQuoteServer.java
import java.io.*; 
import java.net.*; 
import java.util.*; 
 
public class StockQuoteServer { 
    public static int PORT = 2004; 
 
    public static void main(String[] args) throws IOException { 
        ServerSocket serverSocket = new ServerSocket(PORT); 
        while (true) { 
            Socket socket = serverSocket.accept(); 
            StockQuoteThread clientThread = 
                new StockQuoteThread(socket); 
            clientThread.start(); 
        } 
    } 
} 
 
class StockQuoteThread extends Thread { 
    private Socket mSocket; 
    private BufferedReader mSocketReader; 
    private PrintWriter mSocketWriter; 
    private Random mRandomGenerator = new Random(); 
 
    public StockQuoteThread(Socket aSocket) throws IOException { 
        mSocket = aSocket; 
        mSocketReader = new BufferedReader( 
            new InputStreamReader(mSocket.getInputStream())); 
        mSocketWriter = new PrintWriter( 
            new OutputStreamWriter(mSocket.getOutputStream())); 
    } 
 
    public void run() { 
        try { 
            String companyTicker = mSocketReader.readLine(); 
            while (!isInterrupted()) { 
                String quote = companyTicker + ": " + 
                    getDate() + " " + getRandomQuote(); 
                mSocketWriter.println(quote); 
                mSocketWriter.flush(); 
                int delay = mRandomGenerator.nextInt(3000); 
                Thread.sleep(delay); 
            } 
        } catch (Exception ex) { 
            ex.printStackTrace(); 
        } 
    } 
 
    private String getRandomQuote() { 
        int value = mRandomGenerator.nextInt(10000); 
        return " " + (value / 100) + "." + value % 100; 
    } 
 
    private String getDate() { 
        return (new Date()).toString(); 
    } 
}