Разбор тела HTTP в Java

Чего я пытаюсь достичь:

В настоящее время я разрабатываю прокси, целью которого является изменение тела, полученного в ответе сервера. (Прокси должен поддерживать только HTTP, а не HTTPS).

В качестве примера того, что я хочу, чтобы прокси выполнял:

Клиент (браузер) отправляет HTTP-запрос GET на прокси-сервер, затем он анализируется и перенаправляется на правильный хост. Затем хост (сервер) ответит 200 OK файлу HTML. Затем файл HTML анализируется в прокси-сервере и изменяется. Затем прокси-сервер изменяет Content-Length и другие заголовки, если это необходимо, и отправляет его обратно клиенту. Теперь клиент увидит измененную версию HTML-файла, полученного прокси-сервером от сервера.

Проблема:

Похоже, у прокси есть проблема с UTF-8 и другими кодировками, когда шрифт не может распознавать определенные символы. Что происходит, так это то, что когда я читаю с использованием InputStream Socket, время ожидания истекает, потому что он считает, что не прочитал достаточно байтов (в соответствии с Content-Length). Когда HTML-файл возвращается в браузер, появляется множество «ромбов со знаком вопроса внутри». Что, согласно моим исследованиям, происходит, когда шрифт не может загрузить символ. Он может варьироваться между шрифтами.

Он отлично работает на веб-сайтах, на которых нет «странных символов». При чтении тела он останавливается перед чтением всего тела. Например: в одном случае у меня было тело, содержащее 179643 байта, и оно перестало читать, когда мое значение bodyLength имело значение ~ 3000 байт. Затем время ожидания истекло, что вызвало 5-секундную задержку между сервером и клиентом. Содержимое было правильным, просто оно неправильно вычислялось в цикле while.

У меня есть этот фрагмент кода, который вызывает проблемы (этот код обрабатывает ответ на Socket)

private Response getResponse(final Socket socket) {
    try {

        HashMap<String, String> headers;
        StringBuilder builder = new StringBuilder();
        BufferedReader stream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        //-- READ FIRST LINE --//
        // We assume that it is a valid response! (TODO)
        String[] firstLine = stream.readLine().split(" ",3);
        headers = getHeaders(stream);

        //--- GET BODY ---//
        String contentLength = headers.get("Content-Length");
        //Check if body exists
        if (contentLength != null) {
            int bodyLength = Integer.parseInt(contentLength);

            String s;
            //The issue occurs in this while loop!
            while(bodyLength > 0 && (s = stream.readLine()) != null) {
                bodyLength -= (s+"\n").getBytes(StandardCharsets.UTF_8).length;
                builder.append(s).append("\n");
            }

        }

        //-- Return Request --//
        int code = Integer.parseInt(firstLine[1]);
        return new Response(headers,builder, firstLine[0],code, firstLine[2]);
    }
    catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

(ПРИМЕЧАНИЕ. Я знаю, что анализ всего тела как строк неэффективен и что «настоящий» прокси-сервер будет просто передавать байты. Однако, насколько мне известно, я вынужден читать строки, поскольку мне нужно изменить содержимое , Я также должен заявить, что мне не разрешено использовать библиотеки!)

Выше вы можете видеть, что у меня есть StandardCharsets.UTF_8. Это временно, и у меня это есть, потому что страница, время ожидания которой истекло, использовала UTF-8 в качестве кодировки. Я пытаюсь заставить этот пример работать, прежде чем двигаться дальше и реализовывать лучшее решение.

Я считаю, что проблема связана с циклом while в приведенном выше коде.
Что должен делать этот метод:

  1. Разберите первую строку (например, GET URL PROTOCOL).
  2. Получите заголовки из ответа и поместите их в объект HashMap для удобства использования.
  3. Проверяем, есть ли тело, если да, то входим в цикл, в котором делаем шаг 4,5,6 ниже.
  4. Прочитайте строку и добавьте ее в StringBuilder.
  5. Вычтите длину строки из переменной bodyLength, которая является длиной содержимого в виде целого числа.
  6. Цикл, пока bodyLength> 0, потому что, если он равен 0, мы закончили.
  7. Когда закончите, мы можем вернуть весь запрос как объект запроса. (Этот класс является пользовательским и в основном содержит только заголовки, тело и т. д.)

Я только разместил метод выше, так как именно там возникает фактическая «проблема с кодировкой». Увидев другие методы, на мой взгляд, вопрос просто потерял бы свою точность. Если вас интересуют другие части кода, не стесняйтесь задавать вопросы в комментариях!

Теперь собственно вопрос:
Как решить эту проблему? Строки используют UTF-16 в Java, поэтому кодировка «исчезает», когда я читаю строку из InputStream? Например: Если я в приведенном выше фрагменте в начале вместо этого поставил InputStreamReader(socket.getInputStream(), "UTF-8"), то - разве строки не должны быть UTF-8 при чтении из потока? Или они сразу конвертируются в UTF-16, когда устанавливаются как объект String?

Что я пробовал?

Я пытался сделать следующее: InputStreamReader(socket.getInputStream(), "UTF-8") Хотя это, в сочетании с выполнением того же самого для выходного потока, заставляет исчезнуть «ромбики с вопросительными знаками», это не решает проблему тайм-аута.

Я попытался разобрать тело как байты, но почему-то это вообще не сработало. Не только это, но было бы нелегко заменить содержимое тела с помощью этого подхода (о котором я знаю).

Есть причина, по которой HTML-страницы обычно объявляют свою кодировку — это не всегда UTF8, как вы предполагаете. И то, использует ли Java UTF16 внутри, зависит от версии, которую вы используете. В результате вы должны прочитать байты, затем искать кодировку, а затем снова пытаться кодировать.

tquadrat 04.04.2023 21:18

@tquadrat Действительно, я жестко запрограммировал UTF-8, чтобы он работал для одного конкретного примера, как указано в посте. Я исправлю, чтобы он "адаптировался" позже :)

Dubstepzedd 05.04.2023 05:35
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Это неправильно - прочитайте байты, сделайте «математику» о том, сколько байтов осталось, а затем преобразуйте ТЕ. Что не обязательно легко — вы можете читать «половину» символа.

В более общем случае есть библиотеки, которые сделают это за вас. HTTP удивительно сложен, странно хотеть написать целый веб-сервер, особенно когда вы все еще работаете с уровнем опыта, который явно недостаточен для осознания таких основных ошибок. Это не твоя вина; HTTP кажется очень простым, настолько простым, что вы подумали: черт возьми, я попробую. Но не делайте этого.

Одним из сложных аспектов HTTP является то, что это протокол смешанного режима: сам запрос и заголовки основаны на символах, а содержимое основано на байтах. Обратите внимание, что преамбула (заголовки и т. д.) имеет формат US_ASCII. Не UTF8. Обычно это не должно иметь значения (если действительно все отправляется в ASCII, синтаксический анализатор UTF-8 все равно прочитает его), но имеет значение, если ввод недействителен. Я могу рассказать вам несколько очень неприятных историй о том, как принятие вещей, которые не принимают другие серверы, приводит к проблемам с безопасностью, так что не делайте этого.

Есть способы написать это правильно; Конечно, существует множество HTTP-серверов, написанных на Java. Так почему бы не использовать один из них? Например, есть причал, который очень подключаемый и управляемый, и на 100% решение Java. Просто добавьте несколько банок, все, что вам нужно сделать.

Если вы настаиваете на том, чтобы сделать это самостоятельно, знайте, что это всего лишь первый из примерно 5000 вопросов, и шансы на то, что ваш окончательный работающий продукт (если вы когда-нибудь зайдете так далеко) будет действительно «хорошим», практически ничтожны. Практически гарантировано, что у него есть какая-то проблема с безопасностью, возможно, серьезная, и практически гарантировано, что какой-то браузер или сервер, или какая-то экзотическая комбинация того и другого выйдет из строя, если ваш прокси-сервер находится в середине этого.

Если вы настаиваете, это стратегия:

  • Поймите, что HTTP в основном основан на байтах. Если вы напишете new InputStreamReader, вы проиграли игру.
  • Чтобы прочитать «строковые» части, вы читаете данные в байтовой форме до известной конечной точки (например, символ новой строки, сигнализирующий о том, что строка GET /path HTTP/1.1 завершена), а затем берете весь массив байтов, содержащий «строку», и конвертируете это к строке, например. используя new String(byteArr, 0, pos, StandardCharsets.US_ASCII), а затем проанализируйте эту строку (например, сохраните ее на своей карте header или прочитайте из нее метод HTTP).
  • Для тела HTTP-запроса прочитайте байты и передайте их InputStreamReader, но отделите их: вы не можете преобразовать в символы, а затем подсчитать, сколько байтов вы прочитали. Это просто так не работает.
  • ByteBuffer и Channel — это более новый API, и он, вероятно, будет работать намного лучше, особенно если вы хотите эффективно работать с каналом «смешанного режима», который будет отправлять массу данных.
  • ... но, действительно, прервать. Например, механика Range, встроенная в HTTP, используемая для запроса фрагмента ресурса и требуемая более или менее для размещения видео (поскольку веб-видеоплееры постоянно используют это для потоковой передачи видеофайла), совершенно сумасшедшая и не работает. работать так, как можно было бы ожидать. Есть фрагментированное кодирование, которое может быть немного странным. Существуют всевозможные причудливые предостережения, о которых позаботились веб-серверы, вплоть до разбора строки User-Agent для изменения поведения (например, игнорирование указания браузера о том, что они могут обрабатывать сжатие gzip, когда UA говорит, что это IE6 и запрошенный ресурс - css или js. Который IE6 на самом деле не может прочитать при сжатии, даже если он говорит, что может. К счастью, IE6 мертв и похоронен, но это не единственная странная вещь, которую взломал почти каждый веб-сервер. Нет, вы выиграли не найти этого ни в одной спецификации. Это моя точка зрения. Объем знаний в области предметной области, которыми обладают авторы веб-серверов, ошеломляет, и вы потратите следующие 20 лет, заново открывая все это, если попытаетесь написать это самостоятельно. Когда я сказал: « HTTP на самом деле довольно сложен», возможно, теперь вы начинаете понимать, насколько сложно я имею в виду).

Учитывая, что до сих пор вы просто добавляли все это к StringBuilder, т.е. вы, кажется, не заботитесь о том, чтобы иметь дело с очень большим вводом, вы могли бы просто передать все данные в массив байтов, пока они не будут получены ВСЕ, а затем преобразовать весь массив байтов в строку, которая полностью решает текущую проблему, с которой вы столкнулись. Конечно, это не решит 5000 других проблем, с которыми вы столкнетесь в ближайшем будущем.

Я бы очень хотел, чтобы я мог пользоваться библиотекой, но нам это не разрешено, так как это лабораторное задание. Возможно, разрешена какая-то «основная» библиотека синтаксического анализа, но если это так, то это совсем не очевидно. Спасибо, что указали на это и рассказали об основах реализации - это было действительно необходимо. Я буду следовать вашей стратегии и поговорю с моим профессором.

Dubstepzedd 05.04.2023 05:28

Что ж, написание веб-сервера — это... около 3 человеко-лет работы. Я предполагаю, что задание длится так долго? Если нет, то это донкихотское упражнение.

rzwitserloot 05.04.2023 05:35

У нас есть несколько недель, чтобы поработать над этим! Я думаю, что у них нет высоких стандартов.

Dubstepzedd 05.04.2023 06:41

Кстати, можете ли вы показать пример того, что вы имеете в виду под «пропустить через InputStreamReader», если я поместил все байты в ByteBuffer? Как я могу легко преобразовать этот ByteBuffer в текст и не испортить, например, изображения

Dubstepzedd 05.04.2023 07:29

Как только вы войдете в ByteBuffer страну, вы перестанете использовать Reader/InputStream материал из .io. пакет целиком. Но здесь все еще проще (учитывая, что это одноразовое глупое упражнение), прочитать все это в массив байтов, а затем просто new String(thatArr, theCharSetFromTheContentTypeHeader).

rzwitserloot 05.04.2023 14:51

Другие вопросы по теме