From 79aabc55e5333340f496af1c0cc12d88f0adcb4f Mon Sep 17 00:00:00 2001 From: Tomasz Janczuk Date: Tue, 24 Apr 2012 13:38:27 -0700 Subject: [PATCH] fix #152: enable request messages with chunked transfer encoding --- src/iisnode/cnodehttpstoredcontext.cpp | 22 ++++ src/iisnode/cnodehttpstoredcontext.h | 3 + src/iisnode/cprotocolbridge.cpp | 103 +++++++++++++++++- src/iisnode/iisnode.vcxproj | 3 + src/iisnode/iisnode.vcxproj.filters | 12 ++ test/functional/tests/123_upload.js | 41 +++++++ .../tests/node_modules/iisnodeassert.js | 4 +- test/functional/www/123_upload/hello.js | 14 +++ test/functional/www/123_upload/web.config | 7 ++ 9 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 test/functional/tests/123_upload.js create mode 100644 test/functional/www/123_upload/hello.js create mode 100644 test/functional/www/123_upload/web.config diff --git a/src/iisnode/cnodehttpstoredcontext.cpp b/src/iisnode/cnodehttpstoredcontext.cpp index 64194e3..aed75c8 100644 --- a/src/iisnode/cnodehttpstoredcontext.cpp +++ b/src/iisnode/cnodehttpstoredcontext.cpp @@ -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; diff --git a/src/iisnode/cnodehttpstoredcontext.h b/src/iisnode/cnodehttpstoredcontext.h index a1e97c2..0c3f4f3 100644 --- a/src/iisnode/cnodehttpstoredcontext.h +++ b/src/iisnode/cnodehttpstoredcontext.h @@ -48,6 +48,9 @@ public: DWORD GetConnectionRetryCount(); void* GetBuffer(); DWORD GetBufferSize(); + void* GetChunkBuffer(); + DWORD GetChunkBufferSize(); + DWORD GetChunkHeaderMaxSize(); void** GetBufferRef(); DWORD* GetBufferSizeRef(); DWORD GetDataSize(); diff --git a/src/iisnode/cprotocolbridge.cpp b/src/iisnode/cprotocolbridge.cpp index d061ed4..4c72724 100644 --- a/src/iisnode/cprotocolbridge.cpp +++ b/src/iisnode/cprotocolbridge.cpp @@ -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 diff --git a/src/iisnode/iisnode.vcxproj b/src/iisnode/iisnode.vcxproj index 598885c..277852f 100644 --- a/src/iisnode/iisnode.vcxproj +++ b/src/iisnode/iisnode.vcxproj @@ -256,6 +256,7 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P + @@ -313,6 +314,8 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P + + diff --git a/src/iisnode/iisnode.vcxproj.filters b/src/iisnode/iisnode.vcxproj.filters index 222d451..43dbded 100644 --- a/src/iisnode/iisnode.vcxproj.filters +++ b/src/iisnode/iisnode.vcxproj.filters @@ -135,6 +135,9 @@ {53b5e674-69d5-4bae-9629-49cd8b7c7c45} + + {18d24dd3-6582-41ad-856d-949ed0626792} + @@ -588,6 +591,15 @@ Tests\functional\tests + + Tests\functional\www\123_upload + + + Tests\functional\www\123_upload + + + Tests\functional\tests + diff --git a/test/functional/tests/123_upload.js b/test/functional/tests/123_upload.js new file mode 100644 index 0000000..eb49cf3 --- /dev/null +++ b/test/functional/tests/123_upload.js @@ -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(); + }); + } +]); \ No newline at end of file diff --git a/test/functional/tests/node_modules/iisnodeassert.js b/test/functional/tests/node_modules/iisnodeassert.js index 2f2fe14..5e20fbe 100644 --- a/test/functional/tests/node_modules/iisnodeassert.js +++ b/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; diff --git a/test/functional/www/123_upload/hello.js b/test/functional/www/123_upload/hello.js new file mode 100644 index 0000000..0935fda --- /dev/null +++ b/test/functional/www/123_upload/hello.js @@ -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); \ No newline at end of file diff --git a/test/functional/www/123_upload/web.config b/test/functional/www/123_upload/web.config new file mode 100644 index 0000000..ae902f5 --- /dev/null +++ b/test/functional/www/123_upload/web.config @@ -0,0 +1,7 @@ + + + + + + +