Skip to content

Commit 488a3b7

Browse files
Tomasz JanczukTomasz Janczuk
Tomasz Janczuk
authored and
Tomasz Janczuk
committed
first cut of the HTTP upgrade support on IIS 8
1 parent 886abc0 commit 488a3b7

8 files changed

+352
-30
lines changed

Diff for: src/iisnode/chttpprotocol.cpp

+40-5
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,11 @@ HRESULT CHttpProtocol::ParseResponseStatusLine(CNodeHttpStoredContext* context)
304304
// Determine whether to expect response entity body
305305
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
306306

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

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

332335
// adjust buffers
@@ -460,6 +463,7 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)
460463
DWORD offset = 0;
461464
DWORD nameEndOffset, valueEndOffset;
462465
IHttpResponse* response = context->GetHttpContext()->GetResponse();
466+
BOOL needConnectionHeaderFixup = FALSE;
463467

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

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

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

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

501-
CheckError(response->SetHeader(data + offset, data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
507+
if (context->GetIsUpgrade() && 0 == strcmpi("Connection", data + offset))
508+
{
509+
// Add the Connection header under a custom name to force it to be added to unknown headers collection.
510+
// Header name will subsequently be fixed up back to Connection.
511+
// The end result is that the Connection header ends up in the unknown headers collection rather then
512+
// known headers collection where it would get ignored by http.sys.
513+
514+
CheckError(response->SetHeader("x-iisnode-connection", data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
515+
needConnectionHeaderFixup = TRUE;
516+
}
517+
else
518+
{
519+
CheckError(response->SetHeader(data + offset, data + nameEndOffset, valueEndOffset - nameEndOffset, FALSE));
520+
}
502521
}
503522
else if ((valueEndOffset - nameEndOffset) >= 5 && 0 == memcmp((void*)(data + valueEndOffset - 5), "close", 5))
504523
{
@@ -510,11 +529,27 @@ HRESULT CHttpProtocol::ParseResponseHeaders(CNodeHttpStoredContext* context)
510529
context->SetParsingOffset(context->GetParsingOffset() + valueEndOffset - offset + 2);
511530
offset = valueEndOffset + 2;
512531
}
532+
513533
ErrorIf(offset >= dataSize - 1, ERROR_MORE_DATA);
514534
ErrorIf(0x0A != data[offset + 1], ERROR_BAD_FORMAT);
515535

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

538+
if (needConnectionHeaderFixup)
539+
{
540+
HTTP_RESPONSE* rawResponse = response->GetRawHttpResponse();
541+
for (int i = 0; i < response->GetRawHttpResponse()->Headers.UnknownHeaderCount; i++)
542+
{
543+
if (20 == rawResponse->Headers.pUnknownHeaders[i].NameLength
544+
&& 0 == _strnicmp("x-iisnode-connection", rawResponse->Headers.pUnknownHeaders[i].pName, 20))
545+
{
546+
rawResponse->Headers.pUnknownHeaders[i].NameLength = 10;
547+
rawResponse->Headers.pUnknownHeaders[i].pName = "Connection";
548+
break;
549+
}
550+
}
551+
}
552+
518553
return S_OK;
519554
Error:
520555
return hr;

Diff for: src/iisnode/cnodehttpmodule.cpp

+34-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ CNodeHttpModule::CNodeHttpModule(CNodeApplicationManager* applicationManager)
55
{
66
}
77

8+
REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProvider)
9+
{
10+
if (NULL != pHttpContext && NULL != pProvider)
11+
{
12+
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());
13+
DWORD flags = pProvider->GetFlags();
14+
if (NULL != ctx && ctx->GetIsUpgrade() && !ctx->GetOpaqueFlagSet())
15+
{
16+
// Set opaque mode in HTTP.SYS to enable exchanging raw bytes.
17+
18+
19+
pProvider->SetFlags(flags | HTTP_SEND_RESPONSE_FLAG_OPAQUE);
20+
ctx->SetOpaqueFlag();
21+
}
22+
}
23+
24+
return RQ_NOTIFICATION_CONTINUE;
25+
}
26+
827
#if TRUE
928
REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnExecuteRequestHandler(
1029
IN IHttpContext* pHttpContext,
@@ -17,11 +36,11 @@ REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnExecuteRequestHandler(
1736

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

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

23-
PCSTR upgrade = pHttpContext->GetRequest()->GetHeader(HttpHeaderUpgrade, NULL);
24-
ErrorIf(upgrade && 0 == strcmp("websocket", upgrade), ERROR_NOT_SUPPORTED);
42+
//PCSTR upgrade = pHttpContext->GetRequest()->GetHeader(HttpHeaderUpgrade, NULL);
43+
//ErrorIf(upgrade && 0 == strcmp("websocket", upgrade), ERROR_NOT_SUPPORTED);
2544

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

@@ -133,7 +152,18 @@ REQUEST_NOTIFICATION_STATUS CNodeHttpModule::OnAsyncCompletion(
133152
{
134153
if (NULL != pCompletionInfo && NULL != pHttpContext)
135154
{
136-
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());
155+
CNodeHttpStoredContext* ctx = (CNodeHttpStoredContext*)pHttpContext->GetModuleContextContainer()->GetModuleContext(this->applicationManager->GetModuleId());
156+
157+
if (ctx->GetIsUpgrade())
158+
{
159+
IHttpCompletionInfo2* pCompletionInfo2 = (IHttpCompletionInfo2*)pCompletionInfo;
160+
if (1 == pCompletionInfo2->GetCompletedOperation())
161+
{
162+
// This is completion of the read request for incoming bytes of an opaque byte stream after 101 Switching protocol response was sent
163+
164+
ctx = ctx->GetUpgradeContext();
165+
}
166+
}
137167

138168
ctx->IncreasePendingAsyncOperationCount();
139169

Diff for: src/iisnode/cnodehttpmodule.h

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class CNodeHttpModule : public CHttpModule
1717
CNodeHttpModule(CNodeApplicationManager* applicationManager);
1818

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

2223
};

Diff for: src/iisnode/cnodehttpstoredcontext.cpp

+97-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ CNodeHttpStoredContext::CNodeHttpStoredContext(CNodeApplication* nodeApplication
55
chunkLength(0), chunkTransmitted(0), isChunked(FALSE), pipe(INVALID_HANDLE_VALUE), result(S_OK), isLastChunk(FALSE),
66
requestNotificationStatus(RQ_NOTIFICATION_PENDING), connectionRetryCount(0), pendingAsyncOperationCount(1),
77
targetUrl(NULL), targetUrlLength(0), childContext(NULL), isConnectionFromPool(FALSE), expectResponseBody(TRUE),
8-
closeConnection(FALSE)
8+
closeConnection(FALSE), isUpgrade(FALSE), upgradeContext(NULL), opaqueFlagSet(FALSE), requestPumpStarted(FALSE)
99
{
1010
IHttpTraceContext* tctx;
1111
LPCGUID pguid;
@@ -25,7 +25,12 @@ CNodeHttpStoredContext::CNodeHttpStoredContext(CNodeApplication* nodeApplication
2525

2626
CNodeHttpStoredContext::~CNodeHttpStoredContext()
2727
{
28-
if (INVALID_HANDLE_VALUE != this->pipe)
28+
if (NULL != this->upgradeContext)
29+
{
30+
delete this->upgradeContext;
31+
this->upgradeContext = NULL;
32+
}
33+
else if (INVALID_HANDLE_VALUE != this->pipe)
2934
{
3035
CloseHandle(this->pipe);
3136
this->pipe = INVALID_HANDLE_VALUE;
@@ -243,12 +248,26 @@ GUID* CNodeHttpStoredContext::GetActivityId()
243248

244249
long CNodeHttpStoredContext::IncreasePendingAsyncOperationCount()
245250
{
246-
return InterlockedIncrement(&this->pendingAsyncOperationCount);
251+
if (this->requestPumpStarted)
252+
{
253+
return InterlockedIncrement(&this->upgradeContext->pendingAsyncOperationCount);
254+
}
255+
else
256+
{
257+
return InterlockedIncrement(&this->pendingAsyncOperationCount);
258+
}
247259
}
248260

249261
long CNodeHttpStoredContext::DecreasePendingAsyncOperationCount()
250262
{
251-
return InterlockedDecrement(&this->pendingAsyncOperationCount);
263+
if (this->requestPumpStarted)
264+
{
265+
return InterlockedDecrement(&this->upgradeContext->pendingAsyncOperationCount);
266+
}
267+
else
268+
{
269+
return InterlockedDecrement(&this->pendingAsyncOperationCount);
270+
}
252271
}
253272

254273
PCSTR CNodeHttpStoredContext::GetTargetUrl()
@@ -305,4 +324,77 @@ void CNodeHttpStoredContext::SetCloseConnection(BOOL close)
305324
BOOL CNodeHttpStoredContext::GetCloseConnection()
306325
{
307326
return this->closeConnection;
308-
}
327+
}
328+
329+
HRESULT CNodeHttpStoredContext::SetupUpgrade()
330+
{
331+
HRESULT hr;
332+
333+
ErrorIf(this->isUpgrade, E_FAIL);
334+
335+
// The upgradeContext is used to pump incoming bytes to the node.js application.
336+
// The 'this' context is used to pump outgoing bytes to IIS. Once the response headers are flushed,
337+
// both contexts are used concurrently in a full duplex, asynchronous fashion.
338+
// The last context to complete pumping closes the IIS request.
339+
340+
ErrorIf(NULL == (this->upgradeContext = new CNodeHttpStoredContext(this->GetNodeApplication(), this->GetHttpContext())),
341+
ERROR_NOT_ENOUGH_MEMORY);
342+
this->upgradeContext->bufferSize = CModuleConfiguration::GetInitialRequestBufferSize(this->context);
343+
ErrorIf(NULL == (this->upgradeContext->buffer = this->context->AllocateRequestMemory(this->upgradeContext->bufferSize)),
344+
ERROR_NOT_ENOUGH_MEMORY);
345+
346+
// Enable duplex read/write of data
347+
348+
IHttpContext3* ctx3 = (IHttpContext3*)this->GetHttpContext();
349+
ctx3->EnableFullDuplex();
350+
351+
// Disable caching and buffering
352+
353+
ctx3->GetResponse()->DisableBuffering();
354+
ctx3->GetResponse()->DisableKernelCache();
355+
356+
this->upgradeContext->SetPipe(this->GetPipe());
357+
this->upgradeContext->SetNodeProcess(this->GetNodeProcess());
358+
this->upgradeContext->isUpgrade = TRUE;
359+
this->isUpgrade = TRUE;
360+
361+
return S_OK;
362+
363+
Error:
364+
365+
return hr;
366+
}
367+
368+
BOOL CNodeHttpStoredContext::GetIsUpgrade()
369+
{
370+
return this->isUpgrade;
371+
}
372+
373+
CNodeHttpStoredContext* CNodeHttpStoredContext::GetUpgradeContext()
374+
{
375+
return this->upgradeContext;
376+
}
377+
378+
void CNodeHttpStoredContext::SetOpaqueFlag()
379+
{
380+
this->opaqueFlagSet = TRUE;
381+
}
382+
383+
BOOL CNodeHttpStoredContext::GetOpaqueFlagSet()
384+
{
385+
return this->opaqueFlagSet;
386+
}
387+
388+
void CNodeHttpStoredContext::SetRequestPumpStarted()
389+
{
390+
// The pending async operation count for the pair of CNodeHttpStoredContexts will be maintained in the upgradeContext instance from now on.
391+
// The +1 represents the creation of the upgradeContext and the corresponding decrease happens when the pumping of incoming bytes completes.
392+
this->upgradeContext->pendingAsyncOperationCount = this->pendingAsyncOperationCount + 1;
393+
this->pendingAsyncOperationCount = 0;
394+
this->requestPumpStarted = TRUE;
395+
}
396+
397+
BOOL CNodeHttpStoredContext::GetRequestPumpStarted()
398+
{
399+
return this->requestPumpStarted;
400+
}

Diff for: src/iisnode/cnodehttpstoredcontext.h

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class CNodeHttpStoredContext : public IHttpStoredContext
3232
BOOL isConnectionFromPool;
3333
BOOL expectResponseBody;
3434
BOOL closeConnection;
35+
BOOL isUpgrade;
36+
CNodeHttpStoredContext* upgradeContext;
37+
BOOL opaqueFlagSet;
38+
BOOL requestPumpStarted;
3539

3640
public:
3741

@@ -91,6 +95,13 @@ class CNodeHttpStoredContext : public IHttpStoredContext
9195
BOOL GetExpectResponseBody();
9296
void SetCloseConnection(BOOL close);
9397
BOOL GetCloseConnection();
98+
HRESULT SetupUpgrade();
99+
BOOL GetIsUpgrade();
100+
CNodeHttpStoredContext* GetUpgradeContext();
101+
void SetOpaqueFlag();
102+
BOOL GetOpaqueFlagSet();
103+
void SetRequestPumpStarted();
104+
BOOL GetRequestPumpStarted();
94105

95106
static CNodeHttpStoredContext* Get(LPOVERLAPPED overlapped);
96107

0 commit comments

Comments
 (0)