Skip to content

Commit

Permalink
first cut of the HTTP upgrade support on IIS 8
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomasz Janczuk authored and Tomasz Janczuk committed Sep 11, 2012
1 parent 886abc0 commit 488a3b7
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 30 deletions.
45 changes: 40 additions & 5 deletions src/iisnode/chttpprotocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,11 @@ HRESULT CHttpProtocol::ParseResponseStatusLine(CNodeHttpStoredContext* context)
// Determine whether to expect response entity body
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4

if (statusCode >= 100 && statusCode < 200
if (statusCode == 101)
{
CheckError(context->SetupUpgrade());
}
else if (statusCode >= 100 && statusCode < 200
|| statusCode == 204
|| statusCode == 304)
{
Expand All @@ -326,7 +330,6 @@ HRESULT CHttpProtocol::ParseResponseStatusLine(CNodeHttpStoredContext* context)
data[newOffset] = 0; // zero-terminate the reason phrase to reuse it without copying

IHttpResponse* response = context->GetHttpContext()->GetResponse();
//response->Clear();
response->SetStatus(statusCode, data + offset, subStatusCode);

// adjust buffers
Expand Down Expand Up @@ -460,6 +463,7 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)
DWORD offset = 0;
DWORD nameEndOffset, valueEndOffset;
IHttpResponse* response = context->GetHttpContext()->GetResponse();
BOOL needConnectionHeaderFixup = FALSE;

while (offset < (dataSize - 1) && data[offset] != 0x0D)
{
Expand All @@ -486,8 +490,10 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)

data[nameEndOffset] = 0; // zero-terminate name to reuse without copying

// skip the connection header because it relates to the iisnode <-> node.exe communication over named pipes
if (0 != strcmpi("Connection", data + offset))
// Skip the Connection header because it relates to the iisnode <-> node.exe communication over named pipes,
// unless this is a 101 response to HTTP Upgrade request, in which case this is used to inform HTTP.SYS to keep the
// connection open.
if (context->GetIsUpgrade() || 0 != strcmpi("Connection", data + offset))
{
data[valueEndOffset] = 0; // zero-terminate header value because this is what IHttpResponse::SetHeader expects

Expand All @@ -498,7 +504,20 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)
while (*(data + nameEndOffset) == ' ') // data is already zero-terminated, so this loop has sentinel value
nameEndOffset++;

CheckError(response->SetHeader(data + offset, data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
if (context->GetIsUpgrade() && 0 == strcmpi("Connection", data + offset))
{
// Add the Connection header under a custom name to force it to be added to unknown headers collection.
// Header name will subsequently be fixed up back to Connection.
// The end result is that the Connection header ends up in the unknown headers collection rather then
// known headers collection where it would get ignored by http.sys.

CheckError(response->SetHeader("x-iisnode-connection", data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
needConnectionHeaderFixup = TRUE;
}
else
{
CheckError(response->SetHeader(data + offset, data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
}
}
else if ((valueEndOffset - nameEndOffset) >= 5 && 0 == memcmp((void*)(data + valueEndOffset - 5), "close", 5))
{
Expand All @@ -510,11 +529,27 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)
context->SetParsingOffset(context->GetParsingOffset() + valueEndOffset - offset + 2);
offset = valueEndOffset + 2;
}

ErrorIf(offset >= dataSize - 1, ERROR_MORE_DATA);
ErrorIf(0x0A != data[offset + 1], ERROR_BAD_FORMAT);

context->SetParsingOffset(context->GetParsingOffset() + 2);

if (needConnectionHeaderFixup)
{
HTTP_RESPONSE* rawResponse = response->GetRawHttpResponse();
for (int i = 0; i < response->GetRawHttpResponse()->Headers.UnknownHeaderCount; i++)
{
if (20 == rawResponse->Headers.pUnknownHeaders[i].NameLength
&& 0 == _strnicmp("x-iisnode-connection", rawResponse->Headers.pUnknownHeaders[i].pName, 20))
{
rawResponse->Headers.pUnknownHeaders[i].NameLength = 10;
rawResponse->Headers.pUnknownHeaders[i].pName = "Connection";
break;
}
}
}

return S_OK;
Error:
return hr;
Expand Down
38 changes: 34 additions & 4 deletions src/iisnode/cnodehttpmodule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ CNodeHttpModule::CNodeHttpModule(CNodeApplicationManager* applicationManager)
{
}

REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProvider)
{
if (NULL != pHttpContext && NULL != pProvider)
{
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());
DWORD flags = pProvider->GetFlags();
if (NULL != ctx && ctx->GetIsUpgrade() && !ctx->GetOpaqueFlagSet())
{
// Set opaque mode in HTTP.SYS to enable exchanging raw bytes.


pProvider->SetFlags(flags | HTTP_SEND_RESPONSE_FLAG_OPAQUE);
ctx->SetOpaqueFlag();
}
}

return RQ_NOTIFICATION_CONTINUE;
}

#if TRUE
REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnExecuteRequestHandler(
IN IHttpContext* pHttpContext,
Expand All @@ -17,11 +36,11 @@ REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnExecuteRequestHandler(

this->applicationManager->GetEventProvider()->Log(L"iisnode received a new http request", WINEVENT_LEVEL_INFO);

// reject websocket connections since iisnode does not support them
// TODO: reject websocket connections on IIS < 8
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#page-17

PCSTR upgrade = pHttpContext->GetRequest()->GetHeader(HttpHeaderUpgrade, NULL);
ErrorIf(upgrade && 0 == strcmp("websocket", upgrade), ERROR_NOT_SUPPORTED);
//PCSTR upgrade = pHttpContext->GetRequest()->GetHeader(HttpHeaderUpgrade, NULL);
//ErrorIf(upgrade && 0 == strcmp("websocket", upgrade), ERROR_NOT_SUPPORTED);

CheckError(this->applicationManager->Dispatch(pHttpContext, pProvider, &ctx));

Expand Down Expand Up @@ -133,7 +152,18 @@ REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnAsyncCompletion(
{
if (NULL != pCompletionInfo && NULL != pHttpContext)
{
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());

if (ctx->GetIsUpgrade())
{
IHttpCompletionInfo2* pCompletionInfo2 = (IHttpCompletionInfo2*)pCompletionInfo;
if (1 == pCompletionInfo2->GetCompletedOperation())
{
// This is completion of the read request for incoming bytes of an opaque byte stream after 101 Switching protocol response was sent

ctx = ctx->GetUpgradeContext();
}
}

ctx->IncreasePendingAsyncOperationCount();

Expand Down
1 change: 1 addition & 0 deletions src/iisnode/cnodehttpmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class CNodeHttpModule : public CHttpModule
CNodeHttpModule(CNodeApplicationManager* applicationManager);

REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler(IN IHttpContext* pHttpContext, IN IHttpEventProvider* pProvider);
REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProvider);
REQUEST_NOTIFICATION_STATUS OnAsyncCompletion(IHttpContext* pHttpContext, DWORD dwNotification, BOOL fPostNotification, IHttpEventProvider* pProvider, IHttpCompletionInfo* pCompletionInfo);

};
Expand Down
102 changes: 97 additions & 5 deletions src/iisnode/cnodehttpstoredcontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CNodeHttpStoredContext::CNodeHttpStoredContext(CNodeApplication* nodeApplication
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), isConnectionFromPool(FALSE), expectResponseBody(TRUE),
closeConnection(FALSE)
closeConnection(FALSE), isUpgrade(FALSE), upgradeContext(NULL), opaqueFlagSet(FALSE), requestPumpStarted(FALSE)
{
IHttpTraceContext* tctx;
LPCGUID pguid;
Expand All @@ -25,7 +25,12 @@ CNodeHttpStoredContext::CNodeHttpStoredContext(CNodeApplication* nodeApplication

CNodeHttpStoredContext::~CNodeHttpStoredContext()
{
if (INVALID_HANDLE_VALUE != this->pipe)
if (NULL != this->upgradeContext)
{
delete this->upgradeContext;
this->upgradeContext = NULL;
}
else if (INVALID_HANDLE_VALUE != this->pipe)
{
CloseHandle(this->pipe);
this->pipe = INVALID_HANDLE_VALUE;
Expand Down Expand Up @@ -243,12 +248,26 @@ GUID* CNodeHttpStoredContext::GetActivityId()

long CNodeHttpStoredContext::IncreasePendingAsyncOperationCount()
{
return InterlockedIncrement(&this->pendingAsyncOperationCount);
if (this->requestPumpStarted)
{
return InterlockedIncrement(&this->upgradeContext->pendingAsyncOperationCount);
}
else
{
return InterlockedIncrement(&this->pendingAsyncOperationCount);
}
}

long CNodeHttpStoredContext::DecreasePendingAsyncOperationCount()
{
return InterlockedDecrement(&this->pendingAsyncOperationCount);
if (this->requestPumpStarted)
{
return InterlockedDecrement(&this->upgradeContext->pendingAsyncOperationCount);
}
else
{
return InterlockedDecrement(&this->pendingAsyncOperationCount);
}
}

PCSTR CNodeHttpStoredContext::GetTargetUrl()
Expand Down Expand Up @@ -305,4 +324,77 @@ void CNodeHttpStoredContext::SetCloseConnection(BOOL close)
BOOL CNodeHttpStoredContext::GetCloseConnection()
{
return this->closeConnection;
}
}

HRESULT CNodeHttpStoredContext::SetupUpgrade()
{
HRESULT hr;

ErrorIf(this->isUpgrade, E_FAIL);

// The upgradeContext is used to pump incoming bytes to the node.js application.
// The 'this' context is used to pump outgoing bytes to IIS. Once the response headers are flushed,
// both contexts are used concurrently in a full duplex, asynchronous fashion.
// The last context to complete pumping closes the IIS request.

ErrorIf(NULL == (this->upgradeContext = new CNodeHttpStoredContext(this->GetNodeApplication(), this->GetHttpContext())),
ERROR_NOT_ENOUGH_MEMORY);
this->upgradeContext->bufferSize = CModuleConfiguration::GetInitialRequestBufferSize(this->context);
ErrorIf(NULL == (this->upgradeContext->buffer = this->context->AllocateRequestMemory(this->upgradeContext->bufferSize)),
ERROR_NOT_ENOUGH_MEMORY);

// Enable duplex read/write of data

IHttpContext3* ctx3 = (IHttpContext3*)this->GetHttpContext();
ctx3->EnableFullDuplex();

// Disable caching and buffering

ctx3->GetResponse()->DisableBuffering();
ctx3->GetResponse()->DisableKernelCache();

this->upgradeContext->SetPipe(this->GetPipe());
this->upgradeContext->SetNodeProcess(this->GetNodeProcess());
this->upgradeContext->isUpgrade = TRUE;
this->isUpgrade = TRUE;

return S_OK;

Error:

return hr;
}

BOOL CNodeHttpStoredContext::GetIsUpgrade()
{
return this->isUpgrade;
}

CNodeHttpStoredContext* CNodeHttpStoredContext::GetUpgradeContext()
{
return this->upgradeContext;
}

void CNodeHttpStoredContext::SetOpaqueFlag()
{
this->opaqueFlagSet = TRUE;
}

BOOL CNodeHttpStoredContext::GetOpaqueFlagSet()
{
return this->opaqueFlagSet;
}

void CNodeHttpStoredContext::SetRequestPumpStarted()
{
// The pending async operation count for the pair of CNodeHttpStoredContexts will be maintained in the upgradeContext instance from now on.
// The +1 represents the creation of the upgradeContext and the corresponding decrease happens when the pumping of incoming bytes completes.
this->upgradeContext->pendingAsyncOperationCount = this->pendingAsyncOperationCount + 1;
this->pendingAsyncOperationCount = 0;
this->requestPumpStarted = TRUE;
}

BOOL CNodeHttpStoredContext::GetRequestPumpStarted()
{
return this->requestPumpStarted;
}
11 changes: 11 additions & 0 deletions src/iisnode/cnodehttpstoredcontext.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class CNodeHttpStoredContext : public IHttpStoredContext
BOOL isConnectionFromPool;
BOOL expectResponseBody;
BOOL closeConnection;
BOOL isUpgrade;
CNodeHttpStoredContext* upgradeContext;
BOOL opaqueFlagSet;
BOOL requestPumpStarted;

public:

Expand Down Expand Up @@ -91,6 +95,13 @@ class CNodeHttpStoredContext : public IHttpStoredContext
BOOL GetExpectResponseBody();
void SetCloseConnection(BOOL close);
BOOL GetCloseConnection();
HRESULT SetupUpgrade();
BOOL GetIsUpgrade();
CNodeHttpStoredContext* GetUpgradeContext();
void SetOpaqueFlag();
BOOL GetOpaqueFlagSet();
void SetRequestPumpStarted();
BOOL GetRequestPumpStarted();

static CNodeHttpStoredContext* Get(LPOVERLAPPED overlapped);

Expand Down
Loading

0 comments on commit 488a3b7

Please sign in to comment.