IT • archiv

rus / eng | Логин | Комментарий к колонке | Печать | Почта | Клуб




Колонки


HTTP протокол и работа с Web в Java. Программы для работы с Web

 
( Константин Андрухин )

Для того, чтобы получше понять как работать с HTTP, эта часть статьи будет посвящена написанию простой, но полезной утилиты для иследования Web узлов, рассмотрения работы proxy-серверов, написанию простого кэширующего proxy и простейшего Web сервера.

Простой HTTP клиент

Начну же c написания простого HTTP клиента. Идея этой утилиты проста. Иногда, для исследования защиты сервера, очень удобно подсмотреть какой заголовок он выдаёт браузеру и какие куки он при этом присылает клиенту. Также эта утилита пригодиться, если требуется подменить поле "User-Agent" или послать такие cookie, которые сервер не записывал на ваш компьютер. Программа выполняет одно не очень хитрое действие: она читает заранее приготовленный файл, содержащий HTTP запрос, и отсылает его серверу, а ответ сервера, вместе с HTTP заголовком, записывает в другой файл, имя которого задано в параметрах инициализации.

HTTPClient.java

import java.io.*;
import java.net.*;

class HTTPClient
{
    // первый аргумент — имя файла, содержащего HTTP запрос
    // предполагается, что запрос не будет больше 64 килобайт
    // второй — имя файла, куда будет слит ответ сервера
    public static void main(String args[])
    {
        try
        {
            byte buf[] = new byte[64*1024];
            int r;

            // читаем файл с запросом в переменную header
            FileInputStream fis = new FileInputStream(args[0]);
            r = fis.read(buf);
            String header = new String(buf, 0, r);
            fis.close();

            // выделяем из строки запроса хост, порт и URL ресурса
            // для выделения используется специальнонаписанная ф-ия extract
            String host = extract(header, "Host:", "\n");

            // если не найден параметр Host — ошибка
            if(host == null)
            {
                System.out.println("invalid request:\n"+header);
                return;
            }

            // находим порт сервера, по умолчанию он — 80
            int port = host.indexOf(":",0);
            if(port < 0) port = 80;
            else
            {
                port = Integer.parseInt(host.substring(port+1));
                host = host.substring(0, port);
            }

            // открываем сокет до сервера
            Socket s = new Socket(host, port);

            // пишем туда HTTP request
            s.getOutputStream().write(header.getBytes());

            // получаем поток данных от сервера
            InputStream is = s.getInputStream();

            // Открываем для записи файл, куда будет слит лог
            FileOutputStream fos = new FileOutputStream(args[1]);

            // читаем ответ сервера, одновременно сливая его в открытый файл
            r = 1;
            while(r > 0)
            {
                r = is.read(buf);
                if(r > 0)
                    fos.write(buf, 0, r);
            }

            // закрываем файл
            fos.close();
            s.close();
        }
        catch(Exception e)
        {e.printStackTrace();} // вывод исключений
    }

    // "вырезает" из строки str часть,
    // находящуюся между строками start и end
    // если строки end нет, то берётся строка после start
    // если кусок не найден, возвращается null
    // для поиска берётся строка до "\n\n" или "\r\n\r\n",
    // если таковые присутствуют
    protected static String extract(String str, String start, String end)
    {
        int s = str.indexOf("\n\n", 0), e;
        if(s < 0) s = str.indexOf("\r\n\r\n", 0);
        if(s > 0) str = str.substring(0, s);
        s = str.indexOf(start, 0)+start.length();
        if(s < start.length()) return null;
        e = str.indexOf(end, s);
        if(e < 0) e = str.length();
        return (str.substring(s, e)).trim();
    }
}

Компилируем программу, получаем HTTPClient.class и попробуем её использовать: заготовим файл с HTTP заголовком

GET http://www.devresource.org HTTP/1.1
Host: www.devresource.org
User-Agent: HTTPClient

(не забудьте два переноса строки в конце файла) и сохраним его, скажем, как "testrequest.txt" в той же директории, где находится HTTPClient.class, после чего запустим программу:

java HTTPClient testrequest.txt testreply.txt

Если всё пройдёт нормально, сервер devresource.org доступен и работает сеть, через некоторое время вы получите в той же директории файл "testreply.txt", содержащий ответ сервера, включая HTTP заголовок и содержимое документа. Файл "testreply.txt" вы сможете посмотреть в любом текстовом редакторе.

Как создать мэнеджер докачки

Немного модифицировав программу HTTP клиента, вы сможете получить, к примеру, менеджер докачки. Принципы того, как закачать файл не целиком, а только его кусочек, я дал в предыдущей части статьи. Но, на всякий случай, напомню, как это делается:

вначале методом HEAD получаем всю доступную информацию о файле:

HEAD http://www.devresource.org/JavaPower.gif HTTP/1.1
Host: www.devresource.org

из полученного ответа выделяем значение параметра

Content-Length: 6776

теперь, чтобы закачать кусок файла от середины длиною в килобайт, указываем следующий заголовок:

GET http://www.devresource.org/JavaPower.gif HTTP/1.1
Host: www.devresource.org
Range-Unit: 3388 | 1024

если полученный ответ содержит код "206 Partial Content", то всё, что содержится под заголовком, и будет запрашиваемым куском файла.

Простой Web сервер

Web узлы отвечают за выдачу информации HTTP клиентам. Задача следующего примера показать, как работает Web сервер. Для того, чтобы не слишком перегружать код, заранее ограничим её функционально:

  • сервер будет принимать заголовки длиною не более 64 кб
  • сервер понимает только методы GET и POST, в противном случае выдаётся "400 Bad Request"
  • сервер не сможет выдавать документ по-частям (для мэнеджеров докачек)
  • допустимые коды возвратов для сервера ограничиваются "200 OK", если он удачно обработал запрос, "400 Bad Request", если запрос не понят сервером и "404 Not Found", если запрос понят, но файл не найден
  • результатом работы сервера, будет выдача файла, указанного в запросе. При этом всё, что находится после символа "?" в URI документа и сам этот символ отсекаются.
  • MIME типы, выдаваемые сервером ограничены 5 значениями: text/html для файлов с расширениями htm и html, image/jpeg, image/gif, image/x-xbitmap для файлов с расширениями jpg, gif, bmp соответственно и text/plain для всех остальных файлов

Программа SimpleWebServer была создана как модификация SampleServer.java из первой части статьи:

SimpleWebServer.java

import java.io.*;
import java.net.*;
import java.text.DateFormat;
import java.util.Date;
import java.util.TimeZone;

class SimpleWebServer extends Thread
{
    Socket s;

    public static void main(String args[])
    {
        try
        {
            // привинтить сокет на локалхост, порт 80
            ServerSocket server = new ServerSocket(80, 0,
                    InetAddress.getByName("localhost"));

            System.out.println("server is started");

            // слушаем порт
            while(true)
            {
                // ждём нового подключения,
                // после чего запускаем обработку клиента
                // в новый вычислительный поток
                new SimpleWebServer(server.accept());
            }
        }
        catch(Exception e)
        {System.out.println("init error: "+e);} // вывод исключений
    }

    public SimpleWebServer(Socket s)
    {
        this.s = s;

        // и запускаем новый вычислительный поток (см. ф-ю run())
        setDaemon(true);
        setPriority(NORM_PRIORITY);
        start();
    }

    public void run()
    {
        try
        {
            // из сокета клиента берём поток входящих данных
            InputStream is = s.getInputStream();
            // и оттуда же — поток данных от сервера к клиенту
            OutputStream os = s.getOutputStream();

            // буффер данных в 64 килобайта
            byte buf[] = new byte[64*1024];
            // читаем 64кб от клиента, результат —
            // кол-во реально принятых данных
            int r = is.read(buf);

            // создаём строку, содержащую полученую от клиента информацию
            String request = new String(buf, 0, r);

            // получаем путь до документа (см. ниже ф-ю "getPath")
            String path = getPath(request);

            // если из запроса не удалось выделить путь, то
            // возвращаем "400 Bad Request"
            if(path == null)
            {
                // первая строка ответа
                String response = "HTTP/1.1 400 Bad Request\n";

                // дата в GMT
                DateFormat df = DateFormat.getTimeInstance();
                df.setTimeZone(TimeZone.getTimeZone("GMT"));
                response = response + "Date: " +
                 df.format(new Date()) + "\n";

                // остальные заголовки
                response = response
                + "Connection: close\n"
                + "Server: SimpleWebServer\n"
                + "Pragma: no-cache\n\n";

                // выводим данные:
                os.write(response.getBytes());

                // завершаем соединение
                s.close();

                // выход
                return;
            }


            // если файл существует и является директорией,
            // то ищем индексный файл index.html
            File f = new File(path);
            boolean flag = !f.exists();
            if(!flag) if(f.isDirectory())
            {
                if(path.lastIndexOf(""+File.separator) == path.length()-1)
                    path = path + "index.html";
                else
                    path = path + File.separator + "index.html";
                f = new File(path);
                flag = !f.exists();
            }

            // если по указанному пути файл не найден
            // то выводим ошибку "404 Not Found"
            if(flag)
            {
                // первая строка ответа
                String response = "HTTP/1.1 404 Not Found\n";

                // дата в GMT
                DateFormat df = DateFormat.getTimeInstance();
                df.setTimeZone(TimeZone.getTimeZone("GMT"));
                response = response + "Date: " +
                 df.format(new Date()) + "\n";

                // остальные заголовки
                response = response
                + "Content-Type: text/plain\n"
                + "Connection: close\n"
                + "Server: SimpleWebServer\n"
                + "Pragma: no-cache\n\n";

                // и гневное сообщение
                response = response + "File " + path + " not found!";

                // выводим данные:
                os.write(response.getBytes());

                // завершаем соединение
                s.close();

                // выход
                return;
            }

            // определяем MIME файла по расширению
            // MIME по умолчанию — "text/plain"
            String mime = "text/plain";

            // выделяем у файла расширение (по точке)
            r = path.lastIndexOf(".");
            if(r > 0)
            {
                String ext = path.substring(r);
                if(ext.equalsIgnoreCase("html"))
                    mime = "text/html";
                else if(ext.equalsIgnoreCase("htm"))
                    mime = "text/html";
                else if(ext.equalsIgnoreCase("gif"))
                    mime = "image/gif";
                else if(ext.equalsIgnoreCase("jpg"))
                    mime = "image/jpeg";
                else if(ext.equalsIgnoreCase("jpeg"))
                    mime = "image/jpeg";
                else if(ext.equalsIgnoreCase("bmp"))
                    mime = "image/x-xbitmap";
            }

            // создаём ответ

            // первая строка ответа
            String response = "HTTP/1.1 200 OK\n";

            // дата создания в GMT
            DateFormat df = DateFormat.getTimeInstance();
            df.setTimeZone(TimeZone.getTimeZone("GMT"));

            // время последней модификации файла в GMT
            response = response + "Last-Modified: " +
             df.format(new Date(f.lastModified())) + "\n";

            // длина файла
            response = response + "Content-Length: " + f.length() + "\n";

            // строка с MIME кодировкой
            response = response + "Content-Type: " + mime + "\n";

            // остальные заголовки
            response = response
            + "Connection: close\n"
            + "Server: SimpleWebServer\n\n";

            // выводим заголовок:
            os.write(response.getBytes());

            // и сам файл:
            FileInputStream fis = new FileInputStream(path);
            r = 1;
            while(r > 0)
            {
                r = fis.read(buf);
                if(r > 0) os.write(buf, 0, r);
            }
            fis.close();

            // завершаем соединение
            s.close();
        }
        catch(Exception e)
        {e.printStackTrace();} // вывод исключений
    }


    // "вырезает" из HTTP заголовка URI ресурса
    // и конвертирует его в filepath
    // URI берётся только для GET и POST запросов,
    // иначе возвращается null
    protected String getPath(String header)
    {
        // ищем URI, указанный в HTTP запросе
        // URI ищется только для методов POST и GET,
        // иначе возвращается null
        String URI = extract(header, "GET ", " "), path;
        if(URI == null) URI = extract(header, "POST ", " ");
        if(URI == null) return null;

        // если URI записан вместе с именем протокола
        // то удаляем протокол и имя хоста
        path = URI.toLowerCase();
        if(path.indexOf("http://", 0) == 0)
        {
            URI = URI.substring(7);
            URI = URI.substring(URI.indexOf("/", 0));
        }
        else if(path.indexOf("/", 0) == 0)
        // если URI начинается с символа /, удаляем его
            URI = URI.substring(1);

        // отсекаем из URI часть запроса, идущего после символов ? и #
        int i = URI.indexOf("?");
        if(i > 0) URI = URI.substring(0, i);
        i = URI.indexOf("#");
        if(i > 0) URI = URI.substring(0, i);

        // конвертируем URI в путь до документов
        // предполагается, что документы лежат там же, где и сервер
        // иначе ниже нужно переопределить path
        path = "." + File.separator;
        char a;
        for(i = 0; i < URI.length(); i++)
        {
            a = URI.charAt(i);
            if(a == '/')
                path = path + File.separator;
            else
                path = path + a;
        }

        return path;
    }


    // "вырезает" из строки str часть,
    // находящуюся между строками start и end
    // если строки end нет, то берётся строка после start
    // если кусок не найден, возвращается null
    // для поиска берётся строка до "\n\n" или "\r\n\r\n",
    // если таковые присутствуют
    protected String extract(String str, String start, String end)
    {
        int s = str.indexOf("\n\n", 0), e;
        if(s < 0) s = str.indexOf("\r\n\r\n", 0);
        if(s > 0) str = str.substring(0, s);
        s = str.indexOf(start, 0)+start.length();
        if(s < start.length()) return null;
        e = str.indexOf(end, s);
        if(e < 0) e = str.length();
        return (str.substring(s, e)).trim();
    }
}

Компилируем программу и получаем SimpleWebServer.class. Так как данная программа была написана с использованием Java 2 API (в части получения даты в формате GMT), то для её компиляции и выполнения нужен JDK версии не ниже 1.2. Данный сервер будет ставиться на localhost:80, но, в принципе, можно использовать любой другой свободный порт и адрес хоста.

Запускаем сервер:

java SimpleWebServer

Если программа написала "server is started", то сервер запущен и готов к работе. В противном случае, будет выдана ошибка (скорее всего, что даный порт занят другой программой или запрещён политикой безопасности).

Проверьте сервер: положите в директорию, где находится программа файл "index.html". Файл может быть, допустим, таким:

<html><head>test file</head>
<body>
<center> <h1> This a test!!! </h1> </center>
</body></html>

Теперь откройте браузер и наберите адрес "http://localhost" или "http://localhost/". Страничка должна отобразиться.

Proxy серверы

Proxy (proxy — заместитель, посредник) серверы (в просторечье — просто "прокси" или "прокси сервер") — это узловые станции интернета. Они отвечают за соединение различных сегментов интернета меджду собою, а так же могут выполнять несколько других полезных действий. Хотя функционально различные варианты прокси перекрывают друг друга, всё же можно выделить несколько их основных типов.

Шлюз

Первый тип прокси — это так называемый "шлюз". Как было сказанно в первой части статьи, IP адрес для каждой TCP/IP сети должен быть уникальным. Каждый IP в сети интернет тоже уникален, по этому возникает закономерный вопрос подключения частной локальной сети к сети интернет. Этот вопрос как раз и решают шлюзы (они же proxy-gate). Программа такого прокси устанавливается на одном из серверов внутренней сети, имеющий выход в Internet. Разберём принцип работы такого прокси.

Допустим, хост прокси имеет следующий IP адрес во внутренней локальной сети - "127.0.0.2", а порт, на который он установлен — 3128 (наиболее часто используются под прокси следующие порты — 81, 3128, 8080, 8081). Допустим, что клиент, находящийся во внутренней подсети, запрашивает страницу с URL "http://www.devresource.org/". Тогда происходит следующее:

  1. клиент открывает сокет локальной сети до прокси сервера ("127.0.0.2:3128")
  2. в открытый сокет клиент пишет HTTP запрос примерно следующего содержания:
    GET http://www.devresource.org/ HTTP/1.1
    Host: www.devresource.org
    
  3. прокси сервер получает этот запрос, из параметра "Host" узнаёт хост ресурса, его порт и открывает сокет сети Internet до сервера "www.devresource.org:80"
  4. в открытый сокет, прокси-сервер пишет полученый от клиента HTTP запрос; фактически он перенаправляет запрос от клиента к серверу, не изменяя его.
  5. сервер запрашиваемого ресурса получает HTTP запрос от шлюза, обрабатывает его и высылает ответ обратно, к прокси серверу
  6. шлюз получает ответ от сервера "www.devresource.org:80" и, не изменяя его, отправляет к клиенту
  7. клиент получает ответ от сокета прокси сервера и побрабатывает его

Для лучшего понимания того, что пошагово описано выше, прилагаю схему, иллюстрирующую процесс шлюзования запроса:

Схема работы шлюза.
Рисунок 1. Схема работы шлюза.

Анонимный прокси

Следующим типом прокси является "анонимный прокси" или анонимайзер. Принцип его работы схож с работой шлюза, но задача немного не та: задачей анонимайзера является скрыть IP адрес клиента. Этот прокси не пересылает запросы между разными TCP/IP сетями, он просто выступает посредником между клиентом и запрашиваемым хостом.

Анонимный прокси всё так же получает запрос от клиента, обрабатывает поле "Host", передаёт запрос серверу и возвращает его ответ. Единственное отличие в том, что и ServerSocket, и Socket до указанного хоста лежат в одной сети. Собственно, анонимный прокси является самым простым типом прокси-серверов.

Firewall

Ещё одним типом прокси являются так называемые "Firewall" (firewall — огненная стена, в просторечье — файрвол). Это модули системы защиты компьютеров и локальных сетей. Для HTTP суть этих модулей сводится к тому, что они фильтруют нежелательный контент. Например рассмотрим принцип работы простейшего HTTP Firewall, отсекаюего загрузку любых не-текстовых документов и запрещающий запрос страничек, URL которых содержит в себе ключевые слова "sex", "chat" и т.д. У многих на работе стоят подобные файрволы (обычно они функционально совмещены с шлюзами). Зная, как они работают, можно попытаться обойти их.

Итак, разберём шаги, предпринимаемые HTTP файрволом:

  1. клиент открывает сокет локальной сети до прокси сервера и отправляет ему заголовок
  2. файрвол обрабатывает HTTP заголовок запроса: выделяет URL ресурса и сканирует его на наличие "запретных" слов.
    • если слова найдены — возвращает клиенту ошибку типа "403 Forbidden" и завершает с ним соединение
    • если URL ресурса "в порядке", то соединяется с указанным хостом и передаёт ему запрос, в котором подменяет метод запроса на "HEAD". Например так:
      HEAD [URI] HTTP/1.1
      Host: [host]
      
  3. прокси получает часть ответа запрашиваемого сервера и обрабатывает HTTP заголовок ответа: выделяет поле "Content-Type", читает MIME тип документа (по умолчанию, если заголовок "Content-Type" опущен, то считается, что MIME тип - "text/html").
    • если заголовок содержит код возврата отличный от "200 OK", прокси создаёт страничку с информацией об ошибке и отправляет её к клиенту.
    • Если заголовок содержит MIME типа "image/gif" — то есть класса "image/", то в ответ выдаётся заранее заготовленная GIF картинка, содержащая прозрачный пиксел:
      HTTP/1.1 200 OK
      Content-Type: image/gif
      Content-Length: [размер файла в байтах]
      
      [код картинки]
      
      Впрочем, прокси может просто выдать ошибку типа "403 Forbidden" — всё зависит от качества программы.
    • если заголовок содержит MIME типа "text/html" — то есть класса "text/", то прокси перенаправляет запрос клиента серверу, после чего перенаправляет ответ сервера к клиенту (работает как простой посредник)
    • во всех остальных случаях к клиенту возвращается страничка с ошибкой "403 Forbidden"

Кэширующий прокси

Рассмотрим ещё один тип прокси, называемый кэширующим (cache-proxy).

Что это значит кэширующий прокси? Это значит, что такой прокси сохраняет некоторые странички к себе в память, а потом, если от клиента придёт запрос на эту страничку, файл будет загружён не из сети, а из кэша — области памяти, куда прокси сохранил страничку.

Зачем это нужно? Данный принцип значительно сокращает траффик, ведь стоит одному клиенту обратиться, скажем, к "http://www.devresource.org/", как страничка окажется в кэше и для всех следующих клиентов, работающих через данный прокси и запрашивающих "http://www.devresource.org/", серверу не нужно будет снова загружать эту страничку из сети: достаточно будет просто достать её из кэша.

Какие странички нужно кэшировать, а какие — нет регламентируется следующими правилами: прежде всего, сохраняются только те странички, что были получены методом GET запроса. Кроме того, сохранять или не сохранять в кэш регламентируют такие поля HTTP, как "Pragma" и "Cache-Control". Встречаются эти поля, как вы помните, и в HTTP запросе и в HTTP ответе.

Вспомним ещё раз значения полей "Pragma" и "Cache-Control":

  • "public" — документ является публичным, его может брать любой клиент из кэша
  • "private" — документ является приватным, из кэша его может брать только клиент, пославший этот запрос
  • "no-store" — не сохранять документ в кэш
  • "no-transform" — если в кэшэ уже находится документ по данному запросу, то его не нужно обновлять ответом на этот запрос сервера
  • "must-revalidate" — в любом случае, лежит этот документ в кэшэ или нет, прокси обязан обновить его на тот, что выдаст сервер
  • "proxy-revalidate" — относительно прокси-сервера означает то же самое
  • "max-age=[seconds]" — количество секунд, которое должен храниться этот документ в кэшэ, начиная от данного момента

И, как вы помните, значения полей можно совмещать, к примеру:

Pragma: must-revalidate, private, max-age=86400

будет обозначать, что прокси обязан сохранить этот документ в кэш, причём только для этого клиента. И что по истечении суток от данного момента (через 86400 секунд) документ из кэша должен быть удалён.

Замечание
Для прокси-сервера приоритетным является поле "Cache-Control". В случае противоречивых данных в полях "Cache-Control" и "Pragma", прокси сервер будет выполнять команды первого поля, а браузер будет конфигурировать свой кэш в соответствии с полем "Pragma".

Простой кэширующий прокси

Для того, чтобы проиллюстрировать всё то, что рассказано о прокси серверах, я написал программу простейшего кэширующего прокси.

Чтобы не усложнять код, программа имеет следующие ограничения:

  • длина HTTP запроса от клиента не должна превышать 64 кб (чтобы не обрабатывать поле "Content-Length" при POST запросах)
  • обрабатываются только поле "Pragma" из HTTP заголовка запроса; обработка эта заключается в поиске параметра "no-cache"
  • кэш прокси хранится в форме файлов-директорий. К примеру страничка с URL "http://www.devresource.ru/javalinks/catalog.php3?val=this a test!" в системе Windows будет сохранена как ".\cache\www.devresource.org\!javalinks\catalog.php3\val=this%20a%20test%21" - как видно, символы "/" и "?" заменяются, соответственно, на "\!" и "\" (вместо "\" может быть любой символ разделителя пути — берётся java.io.File.separatorChar), а служебный символ "!" обозначающий директорию в кэше и непечатные символы — на %NN, в соответствие с кодом символа.

Программа CacheProxy была тоже создана как модификация SampleServer.java из первой части статьи:

CacheProxy.java

import java.io.*;
import java.net.*;

class CacheProxy extends Thread
{
    Socket s; // сокет подключения
    InputStream is; // входящий поток от сокета
    OutputStream os; // исходящий поток от сокета

    // пытается подключиться как сервер на адрес localhost порт 3128
    // после чего сидит и ждёт подключений от браузера
    // каждое новое подключение передаёт
    // в обработку отдельному вычислительному потоку
    public static void main(String args[])
    {
        try
        {
            // bind to "localhost:3128"
            ServerSocket s = new ServerSocket(3128, 0,
             InetAddress.getByName("localhost"));

            System.out.println("proxy is started");

            // listen port
            while(true)
            {
            // process new client in new thread
                try {new CacheProxy(s.accept());}
                catch(Exception ex) {}
            }
        }
        // by socket binding error
        catch(Exception e)
        {System.out.println("main init error: "+e);}
    }

    // конструктор потока обработки подключения
    public CacheProxy(Socket s) throws Exception
    {
        this.s = s;

        // start thread
        setDaemon(true);
        setPriority(NORM_PRIORITY);
        start();
    }

    // "вырезает" из строки str часть,
    // находящуюся между строками start и end
    // если строки end нет, то берётся строка после start
    // если кусок не найден, возвращается null
    // для поиска берётся строка до "\n\n" или
    // "\r\n\r\n", если таковые присутствуют
    protected String extract(String str, String start, String end)
    {
        int s = str.indexOf("\n\n", 0), e;
        if(s < 0) s = str.indexOf("\r\n\r\n", 0);
        if(s > 0) str = str.substring(0, s);
        s = str.indexOf(start, 0)+start.length();
        if(s < start.length()) return null;
        e = str.indexOf(end, s);
        if(e < 0) e = str.length();
        return (str.substring(s, e)).trim();
    }

    // "вырезает" из HTTP заголовка URI ресурса
    // и конвертирует его в filepath для файла кэша
    // URI берётся только для GET и POST запросов, иначе возвращается null
    protected String getPath(String header)
    {
        String URI = extract(header, "GET ", " "), path;
        if(URI == null) URI = extract(header, "POST ", " ");
        if(URI == null) return null;

        path = URI.toLowerCase();
        if(path.indexOf("http://", 0) == 0)
            URI = URI.substring(7);
        else
        {
            path = extract(header, "Host:", "\n");
            if(path == null) return null;
            URI = path+URI;
        }

        // define cashe path
        path = "cache"+File.separator;

        // convert URI to filepath
        char a;
        boolean flag = false;
        for(int i = 0; i < URI.length(); i++)
        {
            a = URI.charAt(i);

            switch(a)
            {
            case '/' :
                if(flag)
                 path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                else
                 path = path+".!"+File.separatorChar;
                break;
            case '!' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '\\' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case ':' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '*' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '?' :
                if(flag)
                 path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                else
                {
                 path = path+".!"+File.separatorChar;
                    flag = true;
                }
                break;
            case '"' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '<' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '>' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            case '|' :
                path = path+"%"+Integer.toString((int)a, 16).toUpperCase();
                break;
            default: path = path+a;
            }
        }
        if(path.charAt(path.length()-1) ==
          File.separatorChar) path = path+".root";

        return path;
    }

    // печатает ошибку прокси
    protected void printError(String err) throws Exception
    {
        os.write((new String("HTTP/1.1 200 OK\nServer: HomeProxy\n"
                +"Content-Type: text/plain; charset=windows-1251\n\n"
                +err)).getBytes());
    }

    // загружает из сети страничку
    // с одновременным кэшированием её на диск
    // странички в кэше храняться прямо с HTTP заголовком
    protected void from_net(String header, String host,
     int port, String path) throws Exception
    {
        Socket sc = new Socket(host, port);
        sc.getOutputStream().write(header.getBytes());

        InputStream is = sc.getInputStream();

        File f = new File((new File(path)).getParent());
        if(!f.exists()) f.mkdirs();

        FileOutputStream fos = new FileOutputStream(path);

        byte buf[] = new byte[64*1024];
        int r = 1;
        while(r > 0)
        {
            r = is.read(buf);
            if(r > 0)
            {
                fos.write(buf, 0, r);
                if(r > 0) os.write(buf, 0, r);
            }
        }
        fos.close();
        sc.close();
    }

    // вытаскивает из HTTP заголовка хост,
    // порт соединения и путь до файла кэша,
    // после чего вызывает ф-ию загрузки из сети
    protected void from_net(String header) throws Exception
    {
        String host = extract(header, "Host:", "\n"),
          path = getPath(header);
        if((host == null)||(path == null))
        {
            printError("invalid request:\n"+header);
            return;
        }

        int port = host.indexOf(":",0);
        if(port < 0) port = 80;
        else
        {
            port = Integer.parseInt(host.substring(port+1));
            host = host.substring(0, port);
        }

        from_net(header, host, port, path);
    }

    // загружает из кэша файл и выдаёт его
    // если во входящем HTTP заголовке стоит "Pragma: no-cache"
    // или такого файла в кэше нет, то вызывается ф-ия загрузки из сети
    protected void from_cache(String header) throws Exception
    {
        String path = getPath(header);
        if(path == null)
        {
            printError("invalid request:\n"+header);
            return;
        }

        // except "Pragma: no-cache"
        String pragma = extract(header, "Pragma:", "\n");
        if(pragma != null)
        if(pragma.toLowerCase().equals("no-cache"))
        {
            from_net(header);
            return;
        }

        if((new File(path)).exists())
        {
            FileInputStream fis = new FileInputStream(path);
            byte buf[] = new byte[64*1024];
            int r = 1;

            while(r > 0)
            {
                r = fis.read(buf);
                if(r > 0) os.write(buf, 0, r);
            }

            fis.close();
        }
        else
            from_net(header);
    }

    // обработка подключения "в потоке"
    // получает HTTP запрос от браузера
    // если запрос начинается с GET пытается взять файл из кэша
    // иначе — грузит из сети
    public void run()
    {
        try
        {
            is = s.getInputStream();
            os = s.getOutputStream();


            byte buf[] = new byte[64*1024];
            int r = is.read(buf);

            String header = new String(buf, 0, r);
            if(header.indexOf("GET ", 0) == 0)
                from_cache(header);
            else
                from_net(header);

            s.close();
        }
        catch(Exception e)
        {
            try
            {
                e.printStackTrace();
                printError("exception:\n"+e);
                s.close();
            }
            catch(Exception ex){}
        }
    }
}

После компиляции программы, получаем CacheProxy.class и запускаем его (перед запуском убедитесь, что порт 3128 на вашем localhost свободен):

java CacheProxy

Если выдана строка "proxy is started", то прокси был успешно запущен.

Теперь можно проверить его в действии: в настройках своего браузера найдите секцию, где прописываются Proxy сервера и укажите следующий HTTP прокси — имя localhost, порт - 3128. Теперь откройте браузер и немного поползайте по Web ресурсам. Из-за того, что программа имеет ограниченную функциональность, некоторые ресурсы могут не открыться или выдать ошибку. После завершения работы, откройте в той же директории, где лежит программа, папку "cache" и посмотрите, как прокси сохранил просмотренные вами странички.




Справка | Условия Copyright © 1999 — 2010, IT • archiv.
В начало | Логин | Комментарий к колонке | Поиск | Почта