fix #152: enable request messages with chunked transfer encoding

This commit is contained in:
Tomasz Janczuk 2012-04-24 13:38:27 -07:00
Родитель 0f65ec94f0
Коммит 79aabc55e5
9 изменённых файлов: 204 добавлений и 5 удалений

Просмотреть файл

@ -114,6 +114,28 @@ DWORD CNodeHttpStoredContext::GetBufferSize()
return this->bufferSize;
}
void* CNodeHttpStoredContext::GetChunkBuffer()
{
// leave room in the allocated memory buffer for a chunk transfer encoding header that
// will be calculated only after the entity body chunk had been read
return (void*)((char*)this->GetBuffer() + this->GetChunkHeaderMaxSize());
}
DWORD CNodeHttpStoredContext::GetChunkBufferSize()
{
// leave room in the buffer for the chunk header and the CRLF following a chunk
return this->GetBufferSize() - this->GetChunkHeaderMaxSize() - 2;
}
DWORD CNodeHttpStoredContext::GetChunkHeaderMaxSize()
{
// the maximum size of the chunk header
return 64;
}
void** CNodeHttpStoredContext::GetBufferRef()
{
return &this->buffer;

Просмотреть файл

@ -48,6 +48,9 @@ public:
DWORD GetConnectionRetryCount();
void* GetBuffer();
DWORD GetBufferSize();
void* GetChunkBuffer();
DWORD GetChunkBufferSize();
DWORD GetChunkHeaderMaxSize();
void** GetBufferRef();
DWORD* GetBufferSizeRef();
DWORD GetDataSize();

Просмотреть файл

@ -688,6 +688,19 @@ void CProtocolBridge::SendHttpRequestHeaders(CNodeHttpStoredContext* context)
CheckError(request->DeleteHeader(HttpHeaderExpect));
}
// determine if the request body had been chunked; IIS decodes chunked encoding, so it
// must be re-applied when sending the request entity body
USHORT encodingLength;
PCSTR encoding = request->GetHeader(HttpHeaderTransferEncoding, &encodingLength);
if (NULL != encoding && 0 == strnicmp(encoding, "chunked;", encodingLength > 8 ? 8 : encodingLength))
{
context->SetIsChunked(TRUE);
context->SetIsLastChunk(FALSE);
}
// serialize and send request headers
CheckError(CHttpProtocol::SerializeRequestHeaders(context, context->GetBufferRef(), context->GetBufferSizeRef(), &length));
context->SetNextProcessor(CProtocolBridge::SendHttpRequestHeadersCompleted);
@ -794,7 +807,15 @@ void CProtocolBridge::ReadRequestBody(CNodeHttpStoredContext* context)
if (0 < context->GetHttpContext()->GetRequest()->GetRemainingEntityBytes())
{
context->SetNextProcessor(CProtocolBridge::ReadRequestBodyCompleted);
CheckError(context->GetHttpContext()->GetRequest()->ReadEntityBody(context->GetBuffer(), context->GetBufferSize(), TRUE, &bytesReceived, &completionPending));
if (context->GetIsChunked())
{
CheckError(context->GetHttpContext()->GetRequest()->ReadEntityBody(context->GetChunkBuffer(), context->GetChunkBufferSize(), TRUE, &bytesReceived, &completionPending));
}
else
{
CheckError(context->GetHttpContext()->GetRequest()->ReadEntityBody(context->GetBuffer(), context->GetBufferSize(), TRUE, &bytesReceived, &completionPending));
}
}
if (!completionPending)
@ -817,7 +838,17 @@ Error:
{
context->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log(
L"iisnode detected the end of the http request body", WINEVENT_LEVEL_VERBOSE, context->GetActivityId());
CProtocolBridge::StartReadResponse(context);
if (context->GetIsChunked() && !context->GetIsLastChunk())
{
// send the terminating zero-length chunk
CProtocolBridge::ReadRequestBodyCompleted(S_OK, 0, context->GetOverlapped());
}
else
{
CProtocolBridge::StartReadResponse(context);
}
}
else
{
@ -843,7 +874,17 @@ void WINAPI CProtocolBridge::ReadRequestBodyCompleted(DWORD error, DWORD bytesTr
{
ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log(
L"iisnode detected the end of the http request body", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId());
CProtocolBridge::StartReadResponse(ctx);
if (ctx->GetIsChunked() && !ctx->GetIsLastChunk())
{
// send the zero-length last chunk to indicate the end of a chunked entity body
CProtocolBridge::SendRequestBody(ctx, 0);
}
else
{
CProtocolBridge::StartReadResponse(ctx);
}
}
else
{
@ -861,9 +902,62 @@ void CProtocolBridge::SendRequestBody(CNodeHttpStoredContext* context, DWORD chu
GUID activityId;
memcpy(&activityId, context->GetActivityId(), sizeof GUID);
DWORD length;
char* buffer;
if (context->GetIsChunked())
{
// IIS decodes chunked transfer encoding of request entity body. Chunked encoding must be
// re-applied here around the request body data IIS provided in the buffer before it is sent to node.exe.
// This is done by calculating and pre-pending the chunk header to the data in the buffer
// and appending a chunk terminating CRLF to the data in the buffer.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
// Generate the chunk header (from last byte to first)
buffer = (char*)context->GetChunkBuffer(); // first byte of entity body chunk data
*(--buffer) = 0x0A; // LF
*(--buffer) = 0x0D; // CR
if (0 == chunkLength)
{
// this is the end of the request entity body - generate last, zero-length chunk
*(--buffer) = '0';
context->SetIsLastChunk(TRUE);
}
else
{
length = chunkLength;
while (length > 0)
{
DWORD digit = length % 16;
*(--buffer) = (digit < 10) ? ('0' + digit) : ('a' + digit - 10);
length >>= 4;
}
}
// Append CRLF to the entity body chunk
char* end = (char*)context->GetChunkBuffer() + chunkLength; // first byte after the chunk data
*end = 0x0D; // CR
*(++end) = 0x0A; // LF
// Calculate total length of the chunk including framing
length = end - buffer + 1;
}
else
{
length = chunkLength;
buffer = (char*)context->GetBuffer();
}
// send the entity body data to the node.exe process
context->SetNextProcessor(CProtocolBridge::SendRequestBodyCompleted);
if (WriteFile(context->GetPipe(), context->GetBuffer(), chunkLength, NULL, context->InitializeOverlapped()))
if (WriteFile(context->GetPipe(), (void*)buffer, length, NULL, context->InitializeOverlapped()))
{
// completed synchronously
@ -1137,6 +1231,7 @@ void WINAPI CProtocolBridge::ProcessResponseHeaders(DWORD error, DWORD bytesTran
if (0 == contentLengthLength)
{
ctx->SetIsChunked(TRUE);
ctx->SetIsLastChunk(FALSE);
ctx->SetNextProcessor(CProtocolBridge::ProcessChunkHeader);
}
else

Просмотреть файл

@ -256,6 +256,7 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P
<None Include="..\..\test\functional\tests\120_dev_errors_exception.js" />
<None Include="..\..\test\functional\tests\121_watchedFiles.js" />
<None Include="..\..\test\functional\tests\122_multipleResponseHeaders.js" />
<None Include="..\..\test\functional\tests\123_upload.js" />
<None Include="..\..\test\functional\tests\200_samples.bat" />
<None Include="..\..\test\functional\tests\node_modules\iisnodeassert.js" />
<None Include="..\..\test\functional\tests\parts\106_autoupdate_first.js" />
@ -313,6 +314,8 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P
<None Include="..\..\test\functional\www\121_watchedFiles\web.config" />
<None Include="..\..\test\functional\www\122_multipleResponseHeaders\hello.js" />
<None Include="..\..\test\functional\www\122_multipleResponseHeaders\web.config" />
<None Include="..\..\test\functional\www\123_upload\hello.js" />
<None Include="..\..\test\functional\www\123_upload\web.config" />
<None Include="..\..\test\performance\client.bat" />
<None Include="..\..\test\performance\localRun.bat" />
<None Include="..\..\test\performance\readme.txt" />

Просмотреть файл

@ -135,6 +135,9 @@
<Filter Include="Tests\functional\www\122_multipleResponseHeaders">
<UniqueIdentifier>{53b5e674-69d5-4bae-9629-49cd8b7c7c45}</UniqueIdentifier>
</Filter>
<Filter Include="Tests\functional\www\123_upload">
<UniqueIdentifier>{18d24dd3-6582-41ad-856d-949ed0626792}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
@ -588,6 +591,15 @@
<None Include="..\..\test\functional\tests\122_multipleResponseHeaders.js">
<Filter>Tests\functional\tests</Filter>
</None>
<None Include="..\..\test\functional\www\123_upload\hello.js">
<Filter>Tests\functional\www\123_upload</Filter>
</None>
<None Include="..\..\test\functional\www\123_upload\web.config">
<Filter>Tests\functional\www\123_upload</Filter>
</None>
<None Include="..\..\test\functional\tests\123_upload.js">
<Filter>Tests\functional\tests</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="iisnode.rc" />

Просмотреть файл

@ -0,0 +1,41 @@
/*
Uploading data with and without chunked transfer encoding works
*/
var iisnodeassert = require("iisnodeassert");
iisnodeassert.sequence([
iisnodeassert.post(10000, "/123_upload/hello.js", { body: 'abc', chunked: true }, 200, 'true-abc'),
iisnodeassert.post(2000, "/123_upload/hello.js", { body: 'def' }, 200, 'false-def'),
iisnodeassert.post(2000, "/123_upload/hello.js", { body: '', chunked: true, headers: { 'transfer-encoding': 'chunked'} }, 200, 'true-'),
iisnodeassert.post(2000, "/123_upload/hello.js", { body: '' }, 200, 'false-'),
function (next) {
// test for multi-chunk upload
var net = require('net')
, assert = require('assert');
var timeout = setTimeout(function () {
console.error('Timeout occurred');
assert.ok(false, 'request timed out');
next();
}, 2000);
var host = process.env.IISNODETEST_HOST || 'localhost';
var port = process.env.IISNODETEST_PORT || 31415;
var client = net.connect(port, host, function () {
client.setEncoding('utf8');
client.write('POST /123_upload/hello.js HTTP/1.1\r\nHost: ' + host + ':' + port + '\r\nTransfer-Encoding: chunked\r\n\r\n'
+ '3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n');
});
client.on('data', function (data) {
clearTimeout(timeout);
console.log('Received response: ' + data);
assert.ok(data.indexOf('true-abcdefg') > 0, 'Request contains two chunks of data in the entity body');
client.end();
next();
});
}
]);

4
test/functional/tests/node_modules/iisnodeassert.js сгенерированный поставляемый
Просмотреть файл

@ -37,7 +37,9 @@ function request(timeout, path, message, verb, expectedStatusCode, expectedBody,
}
if (body) {
options.headers["Content-Length"] = body.length;
if (!message.chunked) {
options.headers["Content-Length"] = body.length;
}
}
else if (verb === "POST" || verb === "PUT") {
options.headers["Content-Length"] = 0;

Просмотреть файл

@ -0,0 +1,14 @@
var http = require('http');
http.createServer(function (req, res) {
var response = req.headers['transfer-encoding'] === 'chunked' ? 'true-' : 'false-';
req.on('data', function (chunk) {
response += chunk;
});
req.on('end', function () {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(response);
});
}).listen(process.env.PORT);

Просмотреть файл

@ -0,0 +1,7 @@
<configuration>
<system.webServer>
<handlers>
<add name="iisnode" path="hello.js" verb="*" modules="iisnode" />
</handlers>
</system.webServer>
</configuration>