Как правильно отправлять двоичные данные через HTTPS POST?

Я отправляю двоичные данные с клиента (Debian 6.0.3) на сервер (Windows Server 2003). Чтобы обойти большинство брандмауэров, я использую HTTPS POST. Клиент и сервер реализованы с использованием Boost.Asio и OpenSSL. Сначала я реализовал максимально простую версию, и она отлично работала.

Заголовок HTTP:

POST / HTTP/1.1
User-Agent: my custom client v.1

[binary data]

([binary data] не кодируется base64, если это имеет значение)

Затем на другом клиентском компьютере это не удалось (подключено к тому же серверному компьютеру). Поведение не стабильное. Соединение всегда устанавливается нормально (порт 443). В большинстве случаев я успешно передаю SSL-рукопожатие, но сервер не получает данных (почти никаких данных, иногда действительно принимается пакет или два). Иногда я получаю ошибку рукопожатия SSL «короткое чтение». Иногда я получаю неверные данные.

Клиент подключается к серверу, рукопожатия, отправляет заголовок HTTP POST, а затем бесконечно отправляет двоичные данные, пока не произойдет что-то не так. Для теста я использую сгенерированный пользователем SSL-сертификат.

Код сервера:

namespace ssl = boost::asio::ssl;
ssl::context context(io_service, ssl::context::sslv23);
context.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2);
context.use_certificate_chain_file("server.pem");
context.use_private_key_file("server.pem", boost::asio::ssl::context::pem);

ssl::stream<tcp::socket> socket(io_service, context);

// standard connection accepting

socket.async_handshake(ssl::stream_base::server, ...);
...
boost::asio::async_read_until(socket, POST_header, "\r\n\r\n", ...);
...

Код клиента:

ssl::context context(io_service, ssl::context::sslv23);
context.load_verify_file("server.crt");
socket.reset(new ssl::stream<tcp::socket>(io_service, context));
socket->set_verify_mode(ssl::verify_none);

// standard connection

socket.async_handshake(ssl::stream_base::client, ...);
...

(обработка ошибок опущена вместе с не относящимся к делу кодом)

Как видите, это самое простое SSL-соединение. Что не так? Может быть причина в фаерволе?

Я попробовал простой TCP без SSL через тот же порт 443, все работает нормально.

ИЗМЕНИТЬ:

Пробовал добавлять "Content-Type: application/octet-stream", не помогает.

РЕДАКТИРОВАТЬ 2:

Обычно я получаю заголовок HTTP POST в порядке. Затем я отправляю куски данных как chunk-size(4 bytes)chunk(chunk-size bytes).... Сервер получает chunk-size отлично, но потом ничего. Клиент не уведомляет сервер о проблемах (нет ошибок) и продолжает отправлять данные. Иногда сервер может получить фрагмент или два, иногда он получает недействительные chunk-size, но чаще всего ничего.

ИЗМЕНИТЬ 3:

Сравнил захваченный трафик на клиенте и сервере, разницы не нашел.

Решение

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

Отправка через сокет SSL завершается ошибкой, если я использую мультибуферы Boost.Asio в Boost v.1.48 (самый последний на данный момент). Пример:

// data to send, protocol is [packet size: 4 bytes][packet: packet_size bytes]
std::vector<char> packet = ...;
uint32_t packet_size = packet.size();
// prepare buffers
boost::array<boost::asio::const_buffer, 2> bufs = {{boost::asio::buffer(&packet_size, sizeof(packet_size)), boost::asio::buffer(packet)}};
// send multi buffers by single call
boost::asio::async_write(socket, bufs, ...);

Отправка по отдельности packet_size и packet в этом примере решает проблему. Я далек от того, чтобы называть любое подозрительное поведение ошибкой, особенно если оно связано с библиотеками Boost. Но этот действительно выглядит как ошибка. Пробовал на Boost v.1.47 - работает нормально. Пробовал с обычным сокетом TCP (не SSL) - работает нормально. То же самое и в Linux, и в Windows.

Я собираюсь найти любые сообщения об этой проблеме в списке рассылки Asio и сообщу об этом, если ничего не будет найдено.


person Andriy Tylychko    schedule 03.02.2012    source источник
comment
Работает ли он без использования SSL? Вы уверены, что двоичные данные не содержат встроенных новых строк?   -  person Alan Stokes    schedule 03.02.2012
comment
@AlanStokes: это работает, по крайней мере, на других клиентских машинах во время длительных тестов. Внедренные данные могут содержать что угодно, имеет ли это значение?   -  person Andriy Tylychko    schedule 03.02.2012
comment
@AlanStokes: извините, предыдущий комментарий неточен: он работает без SSL на этом компьютере и работает с SSL на других компьютерах. Я просто не могу понять, в чем разница. Эти машины находятся в разных сетях. Вот почему я подозревал брандмауэр, но перепроверил и ничего не нашел.   -  person Andriy Tylychko    schedule 03.02.2012
comment
Я предлагаю вам попробовать Wireshark на рабочем и нерабочем и сравнить. (Конечно, проще с выключенным SSL.)   -  person Alan Stokes    schedule 03.02.2012
comment
@AlanStokes: да, пытался анализировать трафик. С обеих сторон вижу одно и то же: клиент-›сервер: TLSv1 Application Data, Application Data, ..., сервер-›клиент: TCP https -> какой-то порт [ACK]...   -  person Andriy Tylychko    schedule 03.02.2012
comment
@AndyT, есть ли разница в длине пакетов, фрагментации или сообщениях ICMP? Вы пытались изменить некоторые размеры буфера?   -  person Bruno    schedule 03.02.2012
comment
@Bruno: обычно я вижу пару фрагментов данных приложения в пакете в Wireshark. ICMP-сообщений не заметил. Какой размер буфера я должен изменить и для чего? Сравним данные Wireshark на стороне клиента и сервера в понедельник, спасибо за идею   -  person Andriy Tylychko    schedule 04.02.2012
comment
@Bruno: сравнил перехваченные пакеты, различий не нашел   -  person Andriy Tylychko    schedule 13.02.2012


Ответы (3)


Если вам не нужно работать перед веб-сервером, вам не нужно использовать протокол HTTPS. С точки зрения брандмауэра HTTPS выглядит как еще одно SSL-соединение, и он понятия не имеет, что происходит. Поэтому, если вам нужно только передать данные, а не на реальный веб-сервер, используйте только SSL-соединение через порт 443.

Так что просто устраните неполадки с вашим SSL-соединением, проблема не имеет ничего общего с HTTP.


Если вы хотите использовать веб-сервер HTTP, а не пользовательский клиент:

Два момента:

  1. Вам нужно указать Content-Length.
  2. Если вы используете HTTP/1.1, вам необходимо указать заголовок узла.

Самым простым было бы

POST /url HTTP/1.0
User-Agent: my custom client v.1
Content-Type: application/octet-stream
Content-Length: NNN

Actual Content

Или для HTTP/1.1

POST /url HTTP/1.1
Host: www.example.com
User-Agent: my custom client v.1
Content-Type: application/octet-stream
Content-Length: NNN

Actual Content

Примечание. нельзя отправлять бесконечные данные. Протокол HTTP требует фиксированной длины содержимого, и в большинстве случаев веб-серверы сначала загружают данные, прежде чем передавать их на сервер.

Таким образом, вам придется передавать данные кусками.

person Artyom    schedule 03.02.2012
comment
Некоторые брандмауэры используют прозрачные прокси для управления соединениями SSL, таким образом имитируя HTTPS. И клиент, и сервер реализованы мной. Я не знаю content-length, потому что мои данные представляют собой бесконечный поток, но я попытаюсь указать какое-то большое значение, чтобы проверить, имеет ли это значение. Обычно я получаю заголовок HTTP POST и размер первого блока данных на сервере. Добавил немного информации в вопрос. - person Andriy Tylychko; 03.02.2012
comment
Если вы заранее не знаете размер потока, НЕ используйте заголовок Content-Length вообще. Его значение ДОЛЖНО соответствовать общему отправляемому размеру. Вам нужно будет отправлять потоковые данные порциями, используя вместо этого запрос HTTP 1.1 с заголовком Transfer-Encoding: chunked. - person Remy Lebeau; 03.02.2012
comment
@Andy T Это означает, что прозрачный прокси-сервер знает ваш закрытый ключ ... или клиент SSL игнорирует атаку «человек посередине» или не проверяет сертификат SSL. - person Artyom; 04.02.2012
comment
@RemyLebeau-TeamB: проблема решена, см. мой ответ или обновленный вопрос - person Andriy Tylychko; 15.02.2012

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

Отправка через сокет SSL завершается ошибкой, если я использую мультибуферы Boost.Asio в Boost v.1.48 (самый последний на данный момент). Пример:

// data to send, protocol is [packet size: 4 bytes][packet: packet_size bytes]
std::vector<char> packet = ...;
uint32_t packet_size = packet.size();
// prepare buffers
boost::array<boost::asio::const_buffer, 2> bufs = {{boost::asio::buffer(&packet_size, sizeof(packet_size)), boost::asio::buffer(packet)}};
// send multi buffers by single call
boost::asio::async_write(socket, bufs, ...);

Отправка по отдельности packet_size и packet в этом примере решает проблему. Я далек от того, чтобы называть любое подозрительное поведение ошибкой, особенно если оно связано с библиотеками Boost. Но этот действительно выглядит как ошибка. Пробовал на Boost v.1.47 - работает нормально. Пробовал с обычным сокетом TCP (не SSL) - работает нормально. То же самое и в Linux, и в Windows.

Я собираюсь найти любые сообщения об этой проблеме в списке рассылки Asio и сообщу об этом, если ничего не будет найдено.

person Andriy Tylychko    schedule 15.02.2012
comment
Спустя год что-нибудь нашли? - person holtavolt; 20.09.2013

(EDIT: я изначально удалил это, потому что понял, что на самом деле он не использует HTTP. После комментария, в котором вы думаете, что у вас может быть прокси-сервер MITM и вы должны использовать правильный HTTP, я восстанавливаю/редактирую.)

POST / HTTP/1.1
User-Agent: my custom client v.1

[binary data]

Будь то двоичные данные или нет, в простом HTTP или с SSL/TLS, вам понадобится заголовок Content-Length или используйте фрагментированное кодирование передачи. Этот этот раздел спецификации HTTP. Заголовок Content-Type тоже был бы полезен.

Кодирование фрагментированной передачи предназначено для случаев, когда вы не обязательно знаете длину потока. (Вам всегда нужны некоторые формы разделителей при отправке данных, хотя бы для того, чтобы надежно определить, когда они заканчиваются.)

При этом вы должны быть в состоянии выяснить, находитесь ли вы за прокси-сервером MITM, который просматривает прикладной уровень поверх SSL/TLS, если вы получаете сертификат, который не является вашим сервером. Если вы все еще получаете успешное рукопожатие с выигранным сертификатом сервера, такого прокси-сервера нет. Даже прокси-сервер HTTP будет использовать CONNECT и ретранслировать все без изменения соединения SSL/TLS (и, следовательно, без изменения исходного псевдо-HTTP сверху).

person Bruno    schedule 03.02.2012
comment
Пробовал добавить Content-Length: 1000000\r\n, ничего не изменилось - person Andriy Tylychko; 03.02.2012
comment
Content-Length должен соответствовать точному размеру того, что вы отправляете, поэтому кодирование по частям может в целом быть подходящим. При этом, похоже, это не причина вашей проблемы, к сожалению. - person Bruno; 03.02.2012
comment
проблема решена, см. мой ответ или обновленный вопрос - person Andriy Tylychko; 15.02.2012