diff --git a/src/iisnode/chttpprotocol.cpp b/src/iisnode/chttpprotocol.cpp index 64b1c81..bc48c89 100644 --- a/src/iisnode/chttpprotocol.cpp +++ b/src/iisnode/chttpprotocol.cpp @@ -270,6 +270,113 @@ Error: return hr; } +HRESULT CHttpProtocol::ParseChunkHeader(CNodeHttpStoredContext* context) +{ + HRESULT hr; + + char* data = (char*)context->GetBuffer() + context->GetParsingOffset(); + char* current; + char* chunkHeaderStart; + DWORD dataSize = context->GetDataSize() - context->GetParsingOffset(); + ULONG chunkLength = 0; + ULONG totalChunkLength = 0; + + // attempt to parse as many response body chunks as there are buffered in memory + + current = data; + do + { + // parse chunk length + + chunkHeaderStart = current; + chunkLength = 0; + while (true) + { + ErrorIf((current - data) >= dataSize, ERROR_MORE_DATA); + if (*current >= 'A' && *current <= 'F') + { + chunkLength <<= 4; + chunkLength += *current - 'A' + 10; + } + else if (*current >= 'a' && *current <= 'f') + { + chunkLength <<= 4; + chunkLength += *current - 'a' + 10; + } + else if (*current >= '0' && *current <= '9') + { + chunkLength <<= 4; + chunkLength += *current - '0'; + } + else + { + ErrorIf(current == chunkHeaderStart, ERROR_BAD_FORMAT); // no hex digits found + break; + } + + current++; + } + + // skip optional extensions + + while (true) + { + ErrorIf((current - data) >= dataSize, ERROR_MORE_DATA); + if (*current == 0x0D) + { + break; + } + + current++; + } + + // LF + + current++; + ErrorIf((current - data) >= dataSize, ERROR_MORE_DATA); + ErrorIf(*current != 0x0A, ERROR_BAD_FORMAT); + current++; + + // remember total length of all parsed chunks before attempting to parse subsequent chunk header + + // set total chunk length to include current chunk content length, previously parsed chunks (with headers), + // plus the CRLF following the current chunk content + totalChunkLength = chunkLength + (ULONG)(current - data) + 2; + current += chunkLength + 2; // chunk content length + CRLF + + } while (chunkLength != 0); // exit when last chunk has been detected + + // if we are here, current buffer contains the header of the last chunk of the response + + context->SetChunkLength(totalChunkLength); + context->SetIsLastChunk(TRUE); + context->SetChunkTransmitted(0); + + return S_OK; + +Error: + + if (ERROR_MORE_DATA != hr) + { + context->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( + L"iisnode failed to parse response body chunk header", WINEVENT_LEVEL_ERROR, context->GetActivityId()); + + return hr; + } + else if (0 < totalChunkLength) + { + // at least one response chunk has been successfuly parsed, but more chunks remain + + context->SetChunkLength(totalChunkLength); + context->SetIsLastChunk(FALSE); + context->SetChunkTransmitted(0); + + return S_OK; + } + + return hr; +} + HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context) { HRESULT hr; diff --git a/src/iisnode/chttpprotocol.h b/src/iisnode/chttpprotocol.h index 4ab7b08..353069a 100644 --- a/src/iisnode/chttpprotocol.h +++ b/src/iisnode/chttpprotocol.h @@ -15,6 +15,7 @@ public: static HRESULT SerializeRequestHeaders(CNodeHttpStoredContext* ctx, void** result, DWORD* resultSize, DWORD* resultLength); static HRESULT ParseResponseStatusLine(CNodeHttpStoredContext* context); static HRESULT ParseResponseHeaders(CNodeHttpStoredContext* context); + static HRESULT ParseChunkHeader(CNodeHttpStoredContext* context); }; #endif \ No newline at end of file diff --git a/src/iisnode/cnodehttpstoredcontext.cpp b/src/iisnode/cnodehttpstoredcontext.cpp index 9a6ae8d..d84b617 100644 --- a/src/iisnode/cnodehttpstoredcontext.cpp +++ b/src/iisnode/cnodehttpstoredcontext.cpp @@ -2,7 +2,7 @@ CNodeHttpStoredContext::CNodeHttpStoredContext(CNodeApplication* nodeApplication, IHttpContext* context) : nodeApplication(nodeApplication), context(context), process(NULL), buffer(NULL), bufferSize(0), dataSize(0), parsingOffset(0), - responseContentLength(0), responseContentTransmitted(0), pipe(INVALID_HANDLE_VALUE), result(S_OK), + chunkLength(0), chunkTransmitted(0), isChunked(FALSE), pipe(INVALID_HANDLE_VALUE), result(S_OK), isLastChunk(FALSE), requestNotificationStatus(RQ_NOTIFICATION_PENDING), connectionRetryCount(0), pendingAsyncOperationCount(1), targetUrl(NULL), targetUrlLength(0), childContext(NULL) { @@ -153,24 +153,44 @@ void CNodeHttpStoredContext::SetParsingOffset(DWORD parsingOffset) this->parsingOffset = parsingOffset; } -LONGLONG CNodeHttpStoredContext::GetResponseContentTransmitted() +LONGLONG CNodeHttpStoredContext::GetChunkTransmitted() { - return this->responseContentTransmitted; + return this->chunkTransmitted; } -LONGLONG CNodeHttpStoredContext::GetResponseContentLength() +LONGLONG CNodeHttpStoredContext::GetChunkLength() { - return this->responseContentLength; + return this->chunkLength; } -void CNodeHttpStoredContext::SetResponseContentTransmitted(LONGLONG length) +void CNodeHttpStoredContext::SetChunkTransmitted(LONGLONG length) { - this->responseContentTransmitted = length; + this->chunkTransmitted = length; } -void CNodeHttpStoredContext::SetResponseContentLength(LONGLONG length) +void CNodeHttpStoredContext::SetChunkLength(LONGLONG length) { - this->responseContentLength = length; + this->chunkLength = length; +} + +BOOL CNodeHttpStoredContext::GetIsChunked() +{ + return this->isChunked; +} + +void CNodeHttpStoredContext::SetIsChunked(BOOL chunked) +{ + this->isChunked = chunked; +} + +void CNodeHttpStoredContext::SetIsLastChunk(BOOL lastChunk) +{ + this->isLastChunk = lastChunk; +} + +BOOL CNodeHttpStoredContext::GetIsLastChunk() +{ + return this->isLastChunk; } HRESULT CNodeHttpStoredContext::GetHresult() diff --git a/src/iisnode/cnodehttpstoredcontext.h b/src/iisnode/cnodehttpstoredcontext.h index 9f7038c..b0a1a6d 100644 --- a/src/iisnode/cnodehttpstoredcontext.h +++ b/src/iisnode/cnodehttpstoredcontext.h @@ -19,14 +19,16 @@ private: DWORD bufferSize; DWORD dataSize; DWORD parsingOffset; - LONGLONG responseContentTransmitted; - LONGLONG responseContentLength; + LONGLONG chunkTransmitted; + LONGLONG chunkLength; + BOOL isChunked; HRESULT result; REQUEST_NOTIFICATION_STATUS requestNotificationStatus; long pendingAsyncOperationCount; PCSTR targetUrl; DWORD targetUrlLength; IHttpContext* childContext; + BOOL isLastChunk; public: @@ -47,8 +49,11 @@ public: DWORD* GetBufferSizeRef(); DWORD GetDataSize(); DWORD GetParsingOffset(); - LONGLONG GetResponseContentTransmitted(); - LONGLONG GetResponseContentLength(); + LONGLONG GetChunkTransmitted(); + LONGLONG GetChunkLength(); + BOOL GetIsChunked(); + void SetIsLastChunk(BOOL lastChunk); + BOOL GetIsLastChunk(); HRESULT GetHresult(); REQUEST_NOTIFICATION_STATUS GetRequestNotificationStatus(); GUID* GetActivityId(); @@ -66,8 +71,9 @@ public: void SetBufferSize(DWORD bufferSize); void SetDataSize(DWORD dataSize); void SetParsingOffset(DWORD parsingOffet); - void SetResponseContentTransmitted(LONGLONG length); - void SetResponseContentLength(LONGLONG length); + void SetChunkTransmitted(LONGLONG length); + void SetChunkLength(LONGLONG length); + void SetIsChunked(BOOL chunked); void SetHresult(HRESULT result); void SetRequestNotificationStatus(REQUEST_NOTIFICATION_STATUS status); LPOVERLAPPED InitializeOverlapped(); diff --git a/src/iisnode/cprotocolbridge.cpp b/src/iisnode/cprotocolbridge.cpp index 860aa44..651713a 100644 --- a/src/iisnode/cprotocolbridge.cpp +++ b/src/iisnode/cprotocolbridge.cpp @@ -758,18 +758,6 @@ void CProtocolBridge::ContinueReadResponse(CNodeHttpStoredContext* context) WINEVENT_LEVEL_VERBOSE, &activityId); } - else if (context->GetResponseContentLength() == -1) - { - // connection termination with chunked transfer encoding indicates end of response - // since we have sent Connection: close HTTP request header to node from SendHttpRequestHeaders - - etw->Log(L"iisnode iniatiated reading http response chunk and synchronously detected the end of the http response", - WINEVENT_LEVEL_VERBOSE, - &activityId); - - // CR: narrow down this condition to orderly pipe closure - CProtocolBridge::FinalizeResponse(context); - } else { // error @@ -844,7 +832,8 @@ void WINAPI CProtocolBridge::ProcessResponseHeaders(DWORD error, DWORD bytesTran contentLength = ctx->GetHttpContext()->GetResponse()->GetHeader(HttpHeaderContentLength, &contentLengthLength); if (0 == contentLengthLength) { - ctx->SetResponseContentLength(-1); + ctx->SetIsChunked(TRUE); + ctx->SetNextProcessor(CProtocolBridge::ProcessChunkHeader); } else { @@ -858,14 +847,16 @@ void WINAPI CProtocolBridge::ProcessResponseHeaders(DWORD error, DWORD bytesTran while (i < contentLengthLength && contentLength[i] >= '0' && contentLength[i] <= '9') length = length * 10 + contentLength[i++] - '0'; - ctx->SetResponseContentLength(length); + ctx->SetIsChunked(FALSE); + ctx->SetIsLastChunk(TRUE); + ctx->SetChunkLength(length); + ctx->SetNextProcessor(CProtocolBridge::ProcessResponseBody); } ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( L"iisnode finished processing http response headers", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); - ctx->SetNextProcessor(CProtocolBridge::ProcessResponseBody); - CProtocolBridge::ProcessResponseBody(S_OK, 0, ctx->GetOverlapped()); + ctx->GetAsyncContext()->completionProcessor(S_OK, 0, ctx->GetOverlapped()); return; Error: @@ -884,6 +875,43 @@ Error: return; } +void WINAPI CProtocolBridge::ProcessChunkHeader(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped) +{ + HRESULT hr; + CNodeHttpStoredContext* ctx = CNodeHttpStoredContext::Get(overlapped); + + ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( + L"iisnode starting to process http response body chunk header", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); + + CheckError(error); + + ctx->SetDataSize(ctx->GetDataSize() + bytesTransfered); + CheckError(CHttpProtocol::ParseChunkHeader(ctx)); + + ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( + L"iisnode finished processing http response body chunk header", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); + + ctx->SetNextProcessor(CProtocolBridge::ProcessResponseBody); + CProtocolBridge::ProcessResponseBody(S_OK, 0, ctx->GetOverlapped()); + + return; + +Error: + + if (ERROR_MORE_DATA == hr) + { + CProtocolBridge::ContinueReadResponse(ctx); + } + else + { + ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( + L"iisnode failed to process response body chunk header", WINEVENT_LEVEL_ERROR, ctx->GetActivityId()); + CProtocolBridge::SendEmptyResponse(ctx, 500, _T("Internal Server Error"), hr); + } + + return; +} + void WINAPI CProtocolBridge::ProcessResponseBody(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped) { HRESULT hr; @@ -895,64 +923,73 @@ void WINAPI CProtocolBridge::ProcessResponseBody(DWORD error, DWORD bytesTransfe ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( L"iisnode starting to process http response body", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); - if (S_OK != error) - { - if (ctx->GetResponseContentLength() == -1) - { - // connection termination with chunked transfer encoding indicates end of response - // since we have sent Connection: close HTTP request header to node from SendHttpRequestHeaders - - ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( - L"iisnode detected the end of the http response", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); - - // CR: check the other commend for finalizing response - CProtocolBridge::FinalizeResponse(ctx); - } - else - { - ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( - L"iisnode failed to read http response body", WINEVENT_LEVEL_ERROR, ctx->GetActivityId()); - CProtocolBridge::SendEmptyResponse(ctx, 500, _T("Internal Server Error"), error); - } - - return; - } + CheckError(error); ctx->SetDataSize(ctx->GetDataSize() + bytesTransfered); if (ctx->GetDataSize() > ctx->GetParsingOffset()) { - // send body data to client + // there is response body data in the buffer - // CR: consider using malloc here (memory can be released after Flush) - - ErrorIf(NULL == (chunk = (HTTP_DATA_CHUNK*) ctx->GetHttpContext()->AllocateRequestMemory(sizeof HTTP_DATA_CHUNK)), ERROR_NOT_ENOUGH_MEMORY); - chunk->DataChunkType = HttpDataChunkFromMemory; - chunk->FromMemory.BufferLength = ctx->GetDataSize() - ctx->GetParsingOffset(); - ErrorIf(NULL == (chunk->FromMemory.pBuffer = ctx->GetHttpContext()->AllocateRequestMemory(chunk->FromMemory.BufferLength)), ERROR_NOT_ENOUGH_MEMORY); - memcpy(chunk->FromMemory.pBuffer, (char*)ctx->GetBuffer() + ctx->GetParsingOffset(), chunk->FromMemory.BufferLength); - - ctx->SetDataSize(0); - ctx->SetParsingOffset(0); - ctx->SetNextProcessor(CProtocolBridge::SendResponseBodyCompleted); - - CheckError(ctx->GetHttpContext()->GetResponse()->WriteEntityChunks( - chunk, - 1, - TRUE, - ctx->GetResponseContentLength() == -1 || ctx->GetResponseContentLength() > (ctx->GetResponseContentTransmitted() + chunk->FromMemory.BufferLength), - &bytesSent, - &completionExpected)); - - ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( - L"iisnode started sending http response body chunk", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); - - if (!completionExpected) + if (ctx->GetChunkLength() > ctx->GetChunkTransmitted()) { - CProtocolBridge::SendResponseBodyCompleted(S_OK, chunk->FromMemory.BufferLength, ctx->GetOverlapped()); + // send the smaller of the rest of the current chunk or the data available in the buffer to the client + + DWORD dataInBuffer = ctx->GetDataSize() - ctx->GetParsingOffset(); + DWORD remainingChunkSize = ctx->GetChunkLength() - ctx->GetChunkTransmitted(); + DWORD bytesToSend = dataInBuffer < remainingChunkSize ? dataInBuffer : remainingChunkSize; + + // CR: consider using malloc here (memory can be released after Flush) + + ErrorIf(NULL == (chunk = (HTTP_DATA_CHUNK*) ctx->GetHttpContext()->AllocateRequestMemory(sizeof HTTP_DATA_CHUNK)), ERROR_NOT_ENOUGH_MEMORY); + chunk->DataChunkType = HttpDataChunkFromMemory; + chunk->FromMemory.BufferLength = bytesToSend; + ErrorIf(NULL == (chunk->FromMemory.pBuffer = ctx->GetHttpContext()->AllocateRequestMemory(chunk->FromMemory.BufferLength)), ERROR_NOT_ENOUGH_MEMORY); + memcpy(chunk->FromMemory.pBuffer, (char*)ctx->GetBuffer() + ctx->GetParsingOffset(), chunk->FromMemory.BufferLength); + + if (bytesToSend == dataInBuffer) + { + ctx->SetDataSize(0); + ctx->SetParsingOffset(0); + } + else + { + ctx->SetParsingOffset(ctx->GetParsingOffset() + bytesToSend); + } + + ctx->SetNextProcessor(CProtocolBridge::SendResponseBodyCompleted); + + CheckError(ctx->GetHttpContext()->GetResponse()->WriteEntityChunks( + chunk, + 1, + TRUE, + !ctx->GetIsLastChunk(), + &bytesSent, + &completionExpected)); + + ctx->GetNodeApplication()->GetApplicationManager()->GetEventProvider()->Log( + L"iisnode started sending http response body chunk", WINEVENT_LEVEL_VERBOSE, ctx->GetActivityId()); + + if (!completionExpected) + { + CProtocolBridge::SendResponseBodyCompleted(S_OK, chunk->FromMemory.BufferLength, ctx->GetOverlapped()); + } + } + else if (ctx->GetIsChunked()) + { + // process next chunk of the chunked encoding + + ctx->SetNextProcessor(CProtocolBridge::ProcessChunkHeader); + CProtocolBridge::ProcessChunkHeader(S_OK, 0, ctx->GetOverlapped()); + } + else + { + // response data detected beyond the body length declared with Content-Length + + CheckError(ERROR_BAD_FORMAT); } } - else if (-1 == ctx->GetResponseContentLength() || ctx->GetResponseContentLength() > ctx->GetResponseContentTransmitted()) + else if (ctx->GetIsChunked() || ctx->GetChunkLength() > ctx->GetChunkTransmitted()) { // read more body data @@ -983,11 +1020,15 @@ void WINAPI CProtocolBridge::SendResponseBodyCompleted(DWORD error, DWORD bytesT BOOL completionExpected = FALSE; CheckError(error); - ctx->SetResponseContentTransmitted(ctx->GetResponseContentTransmitted() + bytesTransfered); + ctx->SetChunkTransmitted(ctx->GetChunkTransmitted() + bytesTransfered); - if (ctx->GetResponseContentLength() == -1 || ctx->GetResponseContentLength() > ctx->GetResponseContentTransmitted()) + if (ctx->GetIsLastChunk() && ctx->GetChunkLength() == ctx->GetChunkTransmitted()) { - if (ctx->GetResponseContentLength() == -1 && CModuleConfiguration::GetFlushResponse(ctx->GetHttpContext())) + CProtocolBridge::FinalizeResponse(ctx); + } + else + { + if (ctx->GetIsChunked() && CModuleConfiguration::GetFlushResponse(ctx->GetHttpContext())) { // Flushing of chunked responses is enabled @@ -1002,10 +1043,6 @@ void WINAPI CProtocolBridge::SendResponseBodyCompleted(DWORD error, DWORD bytesT CProtocolBridge::ContinueProcessResponseBodyAfterPartialFlush(S_OK, 0, ctx->GetOverlapped()); } } - else - { - CProtocolBridge::FinalizeResponse(ctx); - } return; Error: @@ -1024,7 +1061,7 @@ void WINAPI CProtocolBridge::ContinueProcessResponseBodyAfterPartialFlush(DWORD CheckError(error); ctx->SetNextProcessor(CProtocolBridge::ProcessResponseBody); - CProtocolBridge::ContinueReadResponse(ctx); + CProtocolBridge::ProcessResponseBody(S_OK, 0, ctx->GetOverlapped()); return; Error: diff --git a/src/iisnode/cprotocolbridge.h b/src/iisnode/cprotocolbridge.h index c3c1ca7..e146ad7 100644 --- a/src/iisnode/cprotocolbridge.h +++ b/src/iisnode/cprotocolbridge.h @@ -30,7 +30,9 @@ private: static void ContinueReadResponse(CNodeHttpStoredContext* context); static void WINAPI ProcessResponseStatusLine(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); static void WINAPI ProcessResponseHeaders(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); - static void WINAPI ProcessResponseBody(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); + + static void WINAPI ProcessChunkHeader(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); + static void WINAPI ProcessResponseBody(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); static void WINAPI SendResponseBodyCompleted(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); static void WINAPI ContinueProcessResponseBodyAfterPartialFlush(DWORD error, DWORD bytesTransfered, LPOVERLAPPED overlapped); diff --git a/src/iisnode/iisnode.vcxproj b/src/iisnode/iisnode.vcxproj index b13c6c7..7f5fdb4 100644 --- a/src/iisnode/iisnode.vcxproj +++ b/src/iisnode/iisnode.vcxproj @@ -238,6 +238,7 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P + @@ -273,6 +274,8 @@ copy /y $(ProjectDir)\..\config\* $(ProjectDir)\..\..\build\$(Configuration)\$(P + + diff --git a/src/iisnode/iisnode.vcxproj.filters b/src/iisnode/iisnode.vcxproj.filters index 605595f..923ccd0 100644 --- a/src/iisnode/iisnode.vcxproj.filters +++ b/src/iisnode/iisnode.vcxproj.filters @@ -108,6 +108,9 @@ {6b173a34-4fbb-49ac-9e03-33cde5a6e50f} + + {5e0f95ef-1256-4223-a412-f5146a7713cc} + @@ -462,5 +465,14 @@ Tests\performance + + Tests\functional\www\113_encoding + + + Tests\functional\www\113_encoding + + + Tests\functional\tests + \ No newline at end of file diff --git a/test/functional/tests/113_encoding.js b/test/functional/tests/113_encoding.js new file mode 100644 index 0000000..93d4e65 --- /dev/null +++ b/test/functional/tests/113_encoding.js @@ -0,0 +1,11 @@ +/* +Fixed length and chunked transfer encoding responses are received. +*/ + +var iisnodeassert = require("iisnodeassert"); + +iisnodeassert.sequence([ + iisnodeassert.get(10000, "/113_encoding/hello.js", 200, "content-length response"), + iisnodeassert.get(2000, "/113_encoding/hello.js?onechunk", 200, "chunked response"), + iisnodeassert.get(4000, "/113_encoding/hello.js?tenchunks", 200, "0123456789") +]); \ No newline at end of file diff --git a/test/functional/www/113_encoding/hello.js b/test/functional/www/113_encoding/hello.js new file mode 100644 index 0000000..502a36b --- /dev/null +++ b/test/functional/www/113_encoding/hello.js @@ -0,0 +1,28 @@ +var http = require('http'); + +http.createServer(function (req, res) { + var query = require('url').parse(req.url).query; + if (query === 'onechunk') { // chunked transfer encoding, one chunk + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('chunked response'); + } + else if (query === 'tenchunks') { // chunked transfer encoding, ten chunks + res.writeHead(200, { 'Content-Type': 'text/html' }); + var n = 0; + function writeOne() { + if (n < 9) { + res.write(n.toString()); + n++; + setTimeout(writeOne, 200); + } + else { + res.end(n.toString()); + } + } + writeOne(); + } + else { // fixed response length with Content-Length + res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': '23' }); + res.end('content-length response'); + } +}).listen(process.env.PORT); \ No newline at end of file diff --git a/test/functional/www/113_encoding/web.config b/test/functional/www/113_encoding/web.config new file mode 100644 index 0000000..ae902f5 --- /dev/null +++ b/test/functional/www/113_encoding/web.config @@ -0,0 +1,7 @@ + + + + + + +