В настоящее время я разрабатываю прокси, целью которого является изменение тела, полученного в ответе сервера. (Прокси должен поддерживать только 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 в приведенном выше коде.
Что должен делать этот метод:
HashMap
для удобства использования.bodyLength
, которая является длиной содержимого в виде целого числа.Я только разместил метод выше, так как именно там возникает фактическая «проблема с кодировкой». Увидев другие методы, на мой взгляд, вопрос просто потерял бы свою точность. Если вас интересуют другие части кода, не стесняйтесь задавать вопросы в комментариях!
Теперь собственно вопрос:
Как решить эту проблему? Строки используют UTF-16 в Java, поэтому кодировка «исчезает», когда я читаю строку из InputStream? Например: Если я в приведенном выше фрагменте в начале вместо этого поставил InputStreamReader(socket.getInputStream(), "UTF-8")
, то - разве строки не должны быть UTF-8 при чтении из потока? Или они сразу конвертируются в UTF-16, когда устанавливаются как объект String
?
Я пытался сделать следующее: InputStreamReader(socket.getInputStream(), "UTF-8")
Хотя это, в сочетании с выполнением того же самого для выходного потока, заставляет исчезнуть «ромбики с вопросительными знаками», это не решает проблему тайм-аута.
Я попытался разобрать тело как байты, но почему-то это вообще не сработало. Не только это, но было бы нелегко заменить содержимое тела с помощью этого подхода (о котором я знаю).
@tquadrat Действительно, я жестко запрограммировал UTF-8, чтобы он работал для одного конкретного примера, как указано в посте. Я исправлю, чтобы он "адаптировался" позже :)
Вы делаете это неправильно. Вам нужны символы, а затем конвертируйте их обратно в байты, чтобы отслеживать длину содержимого.
Это неправильно - прочитайте байты, сделайте «математику» о том, сколько байтов осталось, а затем преобразуйте ТЕ. Что не обязательно легко — вы можете читать «половину» символа.
В более общем случае есть библиотеки, которые сделают это за вас. HTTP удивительно сложен, странно хотеть написать целый веб-сервер, особенно когда вы все еще работаете с уровнем опыта, который явно недостаточен для осознания таких основных ошибок. Это не твоя вина; HTTP кажется очень простым, настолько простым, что вы подумали: черт возьми, я попробую. Но не делайте этого.
Одним из сложных аспектов HTTP является то, что это протокол смешанного режима: сам запрос и заголовки основаны на символах, а содержимое основано на байтах. Обратите внимание, что преамбула (заголовки и т. д.) имеет формат US_ASCII. Не UTF8. Обычно это не должно иметь значения (если действительно все отправляется в ASCII, синтаксический анализатор UTF-8 все равно прочитает его), но имеет значение, если ввод недействителен. Я могу рассказать вам несколько очень неприятных историй о том, как принятие вещей, которые не принимают другие серверы, приводит к проблемам с безопасностью, так что не делайте этого.
Есть способы написать это правильно; Конечно, существует множество HTTP-серверов, написанных на Java. Так почему бы не использовать один из них? Например, есть причал, который очень подключаемый и управляемый, и на 100% решение Java. Просто добавьте несколько банок, все, что вам нужно сделать.
Если вы настаиваете на том, чтобы сделать это самостоятельно, знайте, что это всего лишь первый из примерно 5000 вопросов, и шансы на то, что ваш окончательный работающий продукт (если вы когда-нибудь зайдете так далеко) будет действительно «хорошим», практически ничтожны. Практически гарантировано, что у него есть какая-то проблема с безопасностью, возможно, серьезная, и практически гарантировано, что какой-то браузер или сервер, или какая-то экзотическая комбинация того и другого выйдет из строя, если ваш прокси-сервер находится в середине этого.
Если вы настаиваете, это стратегия:
new InputStreamReader
, вы проиграли игру.GET /path HTTP/1.1
завершена), а затем берете весь массив байтов, содержащий «строку», и конвертируете это к строке, например. используя new String(byteArr, 0, pos, StandardCharsets.US_ASCII)
, а затем проанализируйте эту строку (например, сохраните ее на своей карте header
или прочитайте из нее метод HTTP).InputStreamReader
, но отделите их: вы не можете преобразовать в символы, а затем подсчитать, сколько байтов вы прочитали. Это просто так не работает.ByteBuffer
и Channel
— это более новый API, и он, вероятно, будет работать намного лучше, особенно если вы хотите эффективно работать с каналом «смешанного режима», который будет отправлять массу данных.Range
, встроенная в HTTP, используемая для запроса фрагмента ресурса и требуемая более или менее для размещения видео (поскольку веб-видеоплееры постоянно используют это для потоковой передачи видеофайла), совершенно сумасшедшая и не работает. работать так, как можно было бы ожидать. Есть фрагментированное кодирование, которое может быть немного странным. Существуют всевозможные причудливые предостережения, о которых позаботились веб-серверы, вплоть до разбора строки User-Agent
для изменения поведения (например, игнорирование указания браузера о том, что они могут обрабатывать сжатие gzip, когда UA говорит, что это IE6 и запрошенный ресурс - css или js. Который IE6 на самом деле не может прочитать при сжатии, даже если он говорит, что может. К счастью, IE6 мертв и похоронен, но это не единственная странная вещь, которую взломал почти каждый веб-сервер. Нет, вы выиграли не найти этого ни в одной спецификации. Это моя точка зрения. Объем знаний в области предметной области, которыми обладают авторы веб-серверов, ошеломляет, и вы потратите следующие 20 лет, заново открывая все это, если попытаетесь написать это самостоятельно. Когда я сказал: « HTTP на самом деле довольно сложен», возможно, теперь вы начинаете понимать, насколько сложно я имею в виду).Учитывая, что до сих пор вы просто добавляли все это к StringBuilder, т.е. вы, кажется, не заботитесь о том, чтобы иметь дело с очень большим вводом, вы могли бы просто передать все данные в массив байтов, пока они не будут получены ВСЕ, а затем преобразовать весь массив байтов в строку, которая полностью решает текущую проблему, с которой вы столкнулись. Конечно, это не решит 5000 других проблем, с которыми вы столкнетесь в ближайшем будущем.
Я бы очень хотел, чтобы я мог пользоваться библиотекой, но нам это не разрешено, так как это лабораторное задание. Возможно, разрешена какая-то «основная» библиотека синтаксического анализа, но если это так, то это совсем не очевидно. Спасибо, что указали на это и рассказали об основах реализации - это было действительно необходимо. Я буду следовать вашей стратегии и поговорю с моим профессором.
Что ж, написание веб-сервера — это... около 3 человеко-лет работы. Я предполагаю, что задание длится так долго? Если нет, то это донкихотское упражнение.
У нас есть несколько недель, чтобы поработать над этим! Я думаю, что у них нет высоких стандартов.
Кстати, можете ли вы показать пример того, что вы имеете в виду под «пропустить через InputStreamReader», если я поместил все байты в ByteBuffer? Как я могу легко преобразовать этот ByteBuffer в текст и не испортить, например, изображения
Как только вы войдете в ByteBuffer
страну, вы перестанете использовать Reader
/InputStream
материал из .io. пакет целиком. Но здесь все еще проще (учитывая, что это одноразовое глупое упражнение), прочитать все это в массив байтов, а затем просто new String(thatArr, theCharSetFromTheContentTypeHeader)
.
Есть причина, по которой HTML-страницы обычно объявляют свою кодировку — это не всегда UTF8, как вы предполагаете. И то, использует ли Java UTF16 внутри, зависит от версии, которую вы используете. В результате вы должны прочитать байты, затем искать кодировку, а затем снова пытаться кодировать.