Skip to content

Commit 3f876bd

Browse files
committed
Initial implementation of HTTP compression for Windows
1 parent ac76f07 commit 3f876bd

File tree

8 files changed

+573
-13
lines changed

8 files changed

+573
-13
lines changed

Release/include/cpprest/details/http_helpers.h

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
****/
2828
#pragma once
2929

30+
// CPPREST_EXCLUDE_WEBSOCKETS is a flag that now essentially means "no external dependencies". TODO: Rename
31+
#if defined(_WIN32) && !defined(CPPREST_EXCLUDE_WEBSOCKETS)
32+
#define CPPREST_HTTP_COMPRESSION
33+
#endif
34+
3035
#include "cpprest/http_msg.h"
3136

3237
namespace web { namespace http
@@ -124,4 +129,82 @@ namespace details
124129
_ASYNCRTIMP size_t __cdecl add_chunked_delimiters(_Out_writes_(buffer_size) uint8_t *data, _In_ size_t buffer_size, size_t bytes_read);
125130
}
126131

132+
namespace compression
133+
{
134+
enum class compression_algorithm : int
135+
{
136+
deflate = 15,
137+
gzip = 31,
138+
invalid = 9999
139+
};
140+
141+
using data_buffer = std::vector<uint8_t>;
142+
143+
class stream_decompressor
144+
{
145+
public:
146+
147+
static compression_algorithm to_compression_algorithm(const utility::string_t& alg)
148+
{
149+
if (U("gzip") == alg)
150+
{
151+
return compression_algorithm::gzip;
152+
}
153+
else if (U("deflate") == alg)
154+
{
155+
return compression_algorithm::deflate;
156+
}
157+
158+
return compression_algorithm::invalid;
159+
}
160+
161+
static bool is_supported()
162+
{
163+
#if !defined(CPPREST_HTTP_COMPRESSION)
164+
return false;
165+
#else
166+
return true;
167+
#endif
168+
}
169+
170+
_ASYNCRTIMP stream_decompressor(compression_algorithm alg);
171+
172+
_ASYNCRTIMP data_buffer decompress(const data_buffer& input);
173+
174+
_ASYNCRTIMP data_buffer decompress(const uint8_t* input, size_t input_size);
175+
176+
_ASYNCRTIMP bool has_error() const;
177+
178+
private:
179+
class stream_decompressor_impl;
180+
std::shared_ptr<stream_decompressor_impl> m_pimpl;
181+
};
182+
183+
class stream_compressor
184+
{
185+
public:
186+
187+
static bool is_supported()
188+
{
189+
#if !defined(CPPREST_HTTP_COMPRESSION)
190+
return false;
191+
#else
192+
return true;
193+
#endif
194+
}
195+
196+
_ASYNCRTIMP stream_compressor(compression_algorithm alg);
197+
198+
_ASYNCRTIMP data_buffer compress(const data_buffer& input, bool finish);
199+
200+
_ASYNCRTIMP data_buffer compress(const uint8_t* input, size_t input_size, bool finish);
201+
202+
_ASYNCRTIMP bool has_error() const;
203+
204+
private:
205+
class stream_compressor_impl;
206+
std::shared_ptr<stream_compressor_impl> m_pimpl;
207+
};
208+
209+
}
127210
}}}

Release/include/cpprest/http_client.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class http_client_config
104104
#endif
105105
#if defined(_WIN32) && !defined(__cplusplus_winrt)
106106
, m_buffer_request(false)
107+
, m_request_compressed(false)
107108
#endif
108109
{
109110
}
@@ -301,6 +302,27 @@ class http_client_config
301302
{
302303
m_buffer_request = buffer_request;
303304
}
305+
306+
/// <summary>
307+
/// Checks if requesting a compressed response is turned on, the default is off.
308+
/// </summary>
309+
/// <returns>True if compressed response is enabled, false otherwise</returns>
310+
bool request_compressed_response() const
311+
{
312+
return m_request_compressed;
313+
}
314+
315+
/// <summary>
316+
/// Request that the server responds with a compressed body.
317+
/// If true, in cases where the server does not support compression, this will have no effect.
318+
/// The response body is internally decompressed before the consumer receives the data.
319+
/// </summary>
320+
/// <param name="buffer_request">True to turn on response body compression, false otherwise.</param>
321+
/// <remarks>Please note there is a performance cost due to copying the request data.</remarks>
322+
void set_request_compressed_response(bool request_compressed)
323+
{
324+
m_request_compressed = request_compressed;
325+
}
304326
#endif
305327
#endif
306328

@@ -375,6 +397,7 @@ class http_client_config
375397
#endif
376398
#if defined(_WIN32) && !defined(__cplusplus_winrt)
377399
bool m_buffer_request;
400+
bool m_request_compressed;
378401
#endif
379402
};
380403

Release/src/build/vs14.wod/casablanca140.wod.vcxproj

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), build.root))\Build\Release.Product.Settings" />
2222
<ImportGroup Label="PropertySheets">
2323
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
24+
<Import Project="..\..\..\..\packages\zlib.v140.windesktop.msvcstl.static.rt-dyn.1.2.8.8\build\native\zlib.v140.windesktop.msvcstl.static.rt-dyn.targets" Condition="Exists('..\..\..\..\packages\zlib.v140.windesktop.msvcstl.static.rt-dyn.1.2.8.8\build\native\zlib.v140.windesktop.msvcstl.static.rt-dyn.targets')" />
2425
</ImportGroup>
2526
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
2627
<DebugFileSuffix>d</DebugFileSuffix>
2728
</PropertyGroup>
2829
<PropertyGroup>
2930
<TargetName>$(CppRestBaseFileName)140$(DebugFileSuffix)_$(CppRestSDKVersionFileSuffix).wod</TargetName>
30-
<NuGetPackageImportStamp>57c5697d</NuGetPackageImportStamp>
3131
</PropertyGroup>
32+
<ItemGroup>
33+
<None Include="packages.config" />
34+
</ItemGroup>
3235
<ItemDefinitionGroup>
3336
<ClCompile>
3437
<PreprocessorDefinitions>CPPREST_EXCLUDE_WEBSOCKETS;_ASYNCRT_EXPORT;_PPLX_EXPORT;WIN32;_MBCS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -47,4 +50,10 @@
4750
<Import Project="..\common.vcxitems" Label="Shared" Condition="Exists('..\common.vcxitems')" />
4851
<Import Project="..\win32.vcxitems" Label="Shared" Condition="Exists('..\win32.vcxitems')" />
4952
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
53+
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
54+
<PropertyGroup>
55+
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
56+
</PropertyGroup>
57+
<Error Condition="!Exists('..\..\..\..\packages\zlib.v140.windesktop.msvcstl.static.rt-dyn.1.2.8.8\build\native\zlib.v140.windesktop.msvcstl.static.rt-dyn.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\zlib.v140.windesktop.msvcstl.static.rt-dyn.1.2.8.8\build\native\zlib.v140.windesktop.msvcstl.static.rt-dyn.targets'))" />
58+
</Target>
5059
</Project>

Release/src/http/client/http_client_winhttp.cpp

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ class winhttp_request_context : public request_context
238238

239239
memory_holder m_body_data;
240240

241+
std::unique_ptr<web::http::details::compression::stream_decompressor> decompressor;
242+
241243
virtual void cleanup()
242244
{
243245
if(m_request_handle != nullptr)
@@ -587,6 +589,11 @@ class winhttp_client : public _http_client_communicator
587589
}
588590
}
589591

592+
if(web::http::details::compression::stream_decompressor::is_supported() && client_config().request_compressed_response())
593+
{
594+
msg.headers().add(web::http::header_names::accept_encoding, U("deflate, gzip"));
595+
}
596+
590597
// Add headers.
591598
if(!msg.headers().empty())
592599
{
@@ -1135,6 +1142,24 @@ class winhttp_client : public _http_client_communicator
11351142
}
11361143
}
11371144

1145+
// If the response body is compressed we will read the encoding header and create a decompressor object which will later decompress the body
1146+
utility::string_t encoding;
1147+
if (web::http::details::compression::stream_decompressor::is_supported() && response.headers().match(web::http::header_names::content_encoding, encoding))
1148+
{
1149+
auto alg = web::http::details::compression::stream_decompressor::to_compression_algorithm(encoding);
1150+
1151+
if (alg != web::http::details::compression::compression_algorithm::invalid)
1152+
{
1153+
p_request_context->decompressor = std::make_unique<web::http::details::compression::stream_decompressor>(alg);
1154+
}
1155+
else
1156+
{
1157+
utility::string_t error = U("Unsupported compression algorithm in the Content Encoding header: ");
1158+
error += encoding;
1159+
p_request_context->report_exception(http_exception(error));
1160+
}
1161+
}
1162+
11381163
// Signal that the headers are available.
11391164
p_request_context->complete_headers();
11401165

@@ -1159,12 +1184,21 @@ class winhttp_client : public _http_client_communicator
11591184
case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE :
11601185
{
11611186
// Status information contains pointer to DWORD containing number of bytes available.
1162-
DWORD num_bytes = *(PDWORD)statusInfo;
1187+
const DWORD num_bytes = *(PDWORD)statusInfo;
11631188

11641189
if(num_bytes > 0)
11651190
{
1166-
auto writebuf = p_request_context->_get_writebuffer();
1167-
p_request_context->allocate_reply_space(writebuf.alloc(num_bytes), num_bytes);
1191+
if (p_request_context->decompressor)
1192+
{
1193+
// Decompression is too slow to reliably do on this callback. Therefore we need to store it now in order to decompress it at a later stage in the flow.
1194+
// However, we want to eventually use the writebuf to store the decompressed body. Therefore we'll store the compressed body as an internal allocation in the request_context
1195+
p_request_context->allocate_reply_space(nullptr, num_bytes);
1196+
}
1197+
else
1198+
{
1199+
auto writebuf = p_request_context->_get_writebuffer();
1200+
p_request_context->allocate_reply_space(writebuf.alloc(num_bytes), num_bytes);
1201+
}
11681202

11691203
// Read in body all at once.
11701204
if(!WinHttpReadData(
@@ -1198,7 +1232,7 @@ class winhttp_client : public _http_client_communicator
11981232
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE :
11991233
{
12001234
// Status information length contains the number of bytes read.
1201-
const DWORD bytesRead = statusInfoLength;
1235+
DWORD bytesRead = statusInfoLength;
12021236

12031237
// Report progress about downloaded bytes.
12041238
auto progress = p_request_context->m_request._get_impl()->_progress_handler();
@@ -1220,9 +1254,39 @@ class winhttp_client : public _http_client_communicator
12201254
break;
12211255
}
12221256

1257+
auto writebuf = p_request_context->_get_writebuffer();
1258+
1259+
// If we have compressed data it is stored in the local allocation of the p_request_context. We will store the decompressed buffer in the external allocation of the p_request_context.
1260+
if (p_request_context->decompressor)
1261+
{
1262+
web::http::details::compression::data_buffer decompressed = p_request_context->decompressor->decompress(p_request_context->m_body_data.get(), bytesRead);
1263+
1264+
if (p_request_context->decompressor->has_error())
1265+
{
1266+
p_request_context->report_exception(std::runtime_error("Failed to decompress the response body"));
1267+
return;
1268+
}
1269+
1270+
// We've decompressed this chunk of the body, need to now store it in the writebuffer.
1271+
auto decompressed_size = decompressed.size();
1272+
1273+
if (decompressed_size > 0)
1274+
{
1275+
auto p = writebuf.alloc(decompressed_size);
1276+
p_request_context->allocate_reply_space(p, decompressed_size);
1277+
std::memcpy(p_request_context->m_body_data.get(), &decompressed[0], decompressed_size);
1278+
1279+
bytesRead = decompressed_size;
1280+
}
1281+
else
1282+
{
1283+
p_request_context->report_exception(std::runtime_error("Failed to decompress the response body, empty decompression result"));
1284+
return;
1285+
}
1286+
}
1287+
12231288
// If the data was allocated directly from the buffer then commit, otherwise we still
12241289
// need to write to the response stream buffer.
1225-
auto writebuf = p_request_context->_get_writebuffer();
12261290
if (p_request_context->is_externally_allocated())
12271291
{
12281292
writebuf.commit(bytesRead);

0 commit comments

Comments
 (0)