Skip to content

Add skipIISCustomErrors switch to iisnode #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/config/iisnode_dev_x64.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_dev_x86_on_x64.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_dev_x86_on_x86.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_express_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_express_schema_x64.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false" />
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
1 change: 1 addition & 0 deletions src/config/iisnode_schema_x64.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<attribute name="configOverrides" type="string" expanded="true" defaultValue="iisnode.yml"/>
<attribute name="recycleSignalEnabled" type="bool" defaultValue="false"/>
<attribute name="idlePageOutTimePeriod" type="uint" defaultValue="0" /> <!-- disabled with default value 0 -->
<attribute name="skipIISCustomErrors" type="bool" defaultValue="false"/>"
<attribute name="nodeProcessStickySessions" type="bool" defaultValue="false"/>
</sectionSchema>
</configSchema>
15 changes: 14 additions & 1 deletion src/iisnode/chttpprotocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,20 @@ HRESULT CHttpProtocol::ParseResponseStatusLine(CNodeHttpStoredContext* context)
data[newOffset] = 0; // zero-terminate the reason phrase to reuse it without copying

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

if (CModuleConfiguration::GetSkipIISCustomErrors(context->GetHttpContext()))
{
// set fTrySkipCustomErrors so that error responses sent back from the node app through iisnode
// are passed through to the client instead of being intercepted by IIS when httpErrors existingResponse="Auto"
// this allows a mixed solution where custom error pages can be provided via IIS for errors that occur outside
// of iisnode's purview, while also allowing usually-more-helpful error responses from the node application
// to be passed through to the client rather than being intercepted by IIS.
response->SetStatus(statusCode, data + offset, subStatusCode, S_OK, NULL, TRUE);
}
else
{
response->SetStatus(statusCode, data + offset, subStatusCode);
}

// adjust buffers

Expand Down
50 changes: 30 additions & 20 deletions src/iisnode/cmoduleconfiguration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,10 @@ HRESULT CModuleConfiguration::ApplyConfigOverrideKeyValue(IHttpContext* context,
{
CheckError(GetDWORD(valueStart, &config->idlePageOutTimePeriod));
}
else if(0 == stricmp(keyStart, "skipIISCustomErrors"))
{
CheckError(GetBOOL(valueStart, &config->skipIISCustomErrors));
}

return S_OK;
Error:
Expand Down Expand Up @@ -1258,6 +1262,7 @@ HRESULT CModuleConfiguration::GetConfig(IHttpContext* context, CModuleConfigurat
CheckError(GetString(section, L"nodeProcessCommandLine", &c->nodeProcessCommandLine));
CheckError(GetString(section, L"interceptor", &c->interceptor));
CheckError(GetDWORD(section, L"idlePageOutTimePeriod", &c->idlePageOutTimePeriod));
CheckError(GetBOOL(section, L"skipIISCustomErrors", &c->skipIISCustomErrors, FALSE));

// debuggerPathSegment

Expand Down Expand Up @@ -1504,6 +1509,11 @@ LPWSTR CModuleConfiguration::GetConfigOverrides(IHttpContext* ctx)
GETCONFIG(configOverrides)
}

BOOL CModuleConfiguration::GetSkipIISCustomErrors(IHttpContext* ctx)
{
GETCONFIG(skipIISCustomErrors)
}

HRESULT CModuleConfiguration::GenerateDebuggerConfig(IHttpContext* context, CModuleConfiguration *config)
{
HRESULT hr = S_OK;
Expand Down Expand Up @@ -1592,12 +1602,12 @@ HRESULT CModuleConfiguration::GetDebuggerFilesPathSegmentHelper(
DWORD *pdwDebuggerFilesPathSegmentSize
)
{
HRESULT hr = S_OK;
HCRYPTPROV hProv = 0;
HRESULT hr = S_OK;
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
CHAR rgbDigits[] = "0123456789abcdef";
BYTE rgbHash[32]; // sha256 ==> 32 bytes.
DWORD cbHash = 0;
BYTE rgbHash[32]; // sha256 ==> 32 bytes.
DWORD cbHash = 0;
CHAR shaHash[MAX_HASH_CHAR + 1]; // we will only use first MAX_HASH_CHAR bytes of the sha256 hash ==> 32 hex chars.
DWORD dwSHALength = 0;
CHAR *pInput = NULL;
Expand All @@ -1615,27 +1625,27 @@ HRESULT CModuleConfiguration::GetDebuggerFilesPathSegmentHelper(
ErrorIf(dwInputSize != WideCharToMultiByte(CP_ACP, 0, pszScriptPath, dwScriptPathLen, pInput, dwInputSize, NULL, NULL), E_FAIL);
pInput[dwInputSize] = '\0';

// Get handle to the crypto provider
ErrorIf(!CryptAcquireContext(&hProv,
NULL,
NULL,
PROV_RSA_AES,
CRYPT_VERIFYCONTEXT), HRESULT_FROM_WIN32(GetLastError()));
// Get handle to the crypto provider
ErrorIf(!CryptAcquireContext(&hProv,
NULL,
NULL,
PROV_RSA_AES,
CRYPT_VERIFYCONTEXT), HRESULT_FROM_WIN32(GetLastError()));

ErrorIf(!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash), HRESULT_FROM_WIN32(GetLastError()));

ErrorIf(!CryptHashData(hHash, (BYTE*) pInput, strnlen_s(pInput, dwInputSize), 0), HRESULT_FROM_WIN32(GetLastError()));

// sha256 ==> 32 bytes.
cbHash = 32;
ErrorIf(!CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0), HRESULT_FROM_WIN32(GetLastError()));
dwIndex = 0;
// convert first (MAX_HASH_CHAR / 2) bytes to hexadecimal form.
for (DWORD i = 0; i < (MAX_HASH_CHAR / 2); i++, dwIndex=dwIndex+2)
{
shaHash[dwIndex] = rgbDigits[rgbHash[i] >> 4];
shaHash[dwIndex+1] = rgbDigits[rgbHash[i] & 0xf];
cbHash = 32;
ErrorIf(!CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0), HRESULT_FROM_WIN32(GetLastError()));

dwIndex = 0;
// convert first (MAX_HASH_CHAR / 2) bytes to hexadecimal form.
for (DWORD i = 0; i < (MAX_HASH_CHAR / 2); i++, dwIndex=dwIndex+2)
{
shaHash[dwIndex] = rgbDigits[rgbHash[i] >> 4];
shaHash[dwIndex+1] = rgbDigits[rgbHash[i] & 0xf];
}

shaHash[dwIndex] = '\0';
Expand Down
4 changes: 3 additions & 1 deletion src/iisnode/cmoduleconfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class CModuleConfiguration : public IHttpStoredContext
static BOOL invalid;
SRWLOCK srwlock;
LPWSTR configOverrides;
BOOL skipIISCustomErrors;
BOOL nodeProcessStickySessions;

static IHttpServer* server;
Expand Down Expand Up @@ -130,6 +131,7 @@ class CModuleConfiguration : public IHttpStoredContext
static BOOL GetEnableXFF(IHttpContext* ctx);
static HRESULT GetPromoteServerVars(IHttpContext* ctx, char*** vars, int* count);
static LPWSTR GetConfigOverrides(IHttpContext* ctx);
static BOOL GetSkipIISCustomErrors(IHttpContext* ctx);
static BOOL GetProcessStickySessions(IHttpContext* ctx);

static HRESULT CreateNodeEnvironment(IHttpContext* ctx, DWORD debugPort, PCH namedPipe, PCH signalPipeName, PCH* env);
Expand All @@ -139,4 +141,4 @@ class CModuleConfiguration : public IHttpStoredContext
virtual void CleanupStoredContext();
};

#endif
#endif
4 changes: 4 additions & 0 deletions src/iisnode/cprotocolbridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,11 @@ void CProtocolBridge::SendEmptyResponse(IHttpContext* httpCtx, USHORT status, US
if (!httpCtx->GetResponseHeadersSent())
{
httpCtx->GetResponse()->Clear();

// Internal iisnode errors should probably not set fTrySkipCustomErrors since these are just empty status responses.
// Let IIS capture and replace these responses with more detailed messages depending on the custom error mode.
httpCtx->GetResponse()->SetStatus(status, reason, subStatus, hresult);

if (disableCache)
{
httpCtx->GetResponse()->SetHeader(HttpHeaderCacheControl, "no-cache", 8, TRUE);
Expand Down
5 changes: 4 additions & 1 deletion src/samples/configuration/iisnode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,7 @@ enableXFF: false
# HTTP request headers; for a list of IIS server variables available see
# http://msdn.microsoft.com/en-us/library/ms524602(v=vs.90).aspx; for example "AUTH_USER,AUTH_TYPE"

promoteServerVars:
promoteServerVars:

# skipIISCustomErrors - controls whether iisnode will try to skip IIS <httpErrors> custom error handling when existingResponse="Auto"
skipIISCustomErrors: false
9 changes: 8 additions & 1 deletion src/samples/configuration/readme.htm
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ <h2>
maxRequestBufferSize: 8192 # increasing from the default
# maxConcurrentRequestsPerProcess: 512 - commented out setting

* skipIISCustomErrors - controls whether iisnode will try to skip IIS &gt;httpErrors&lt; custom error handling when existingResponse="Auto"

--&gt;

&lt;iisnode
Expand Down Expand Up @@ -175,6 +177,7 @@ <h2>
enableXFF="false"
promoteServerVars=""
configOverrides="node.conf"
skipIISCustomErrors="false"
/&gt;

&lt;!--
Expand Down Expand Up @@ -335,6 +338,10 @@ <h2>
# HTTP request headers; for a list of IIS server variables available see
# http://msdn.microsoft.com/en-us/library/ms524602(v=vs.90).aspx; for example "AUTH_USER,AUTH_TYPE"

promoteServerVars:</pre>
promoteServerVars:

# skipIISCustomErrors - controls whether iisnode will try to skip IIS &gt;httpErrors&lt; custom error handling when existingResponse="Auto"
skipIISCustomErrors: false
</pre>
</body>
</html>
3 changes: 3 additions & 0 deletions src/samples/configuration/web.config
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
nodeProcessCountPerApplication: 2
maxRequestBufferSize: 8192 # increasing from the default
# maxConcurrentRequestsPerProcess: 512 - commented out setting

* skipIISCustomErrors - controls whether iisnode will try to skip IIS <httpErrors> custom error handling when existingResponse="Auto"

-->

Expand Down Expand Up @@ -140,6 +142,7 @@
enableXFF="false"
promoteServerVars=""
configOverrides="iisnode.yml"
skipIISCustomErrors="false"
/>

<!--
Expand Down
18 changes: 18 additions & 0 deletions test/functional/tests/144_skip_iis_custom_errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var iisnodeassert = require("iisnodeassert");

iisnodeassert.sequence([
// when skipIISCustomErrors="true", error responses returned by the node application should pass through to the client
// when skipIISCustomErrors="false", IIS can intercept error responses from the node application with custom error handling based on the error code
// this option only comes into play when httpErrors existingResponse="Auto".
// existingResponse="PassThrough" and existingResponse="Replace" will always pass error messages from the node app through to the client or replace them, respectively
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/on/create_error.js", 400, 'App created error. Gets replaced by IIS Custom error if skipIISCustomErrors="false"'),
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/off/create_error.js", 400, 'Bad Request'),

// iisnode skipIISCustomErrors option should not affect custom error routing for errors outside of iisnode's purview
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/on/access_denied.js", 401, 'IIS Custom Error page still gets served for non iisnode error responses'),
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/off/access_denied.js", 401, 'IIS Custom Error page still gets served for non iisnode error responses'),

// internal iisnode errors should still get extra handling by IIS depending on error mode
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/on/malformed.js", 500, 'The page cannot be displayed because an internal server error has occurred.'),
iisnodeassert.get(10000, "/143_skip_iis_custom_errors/off/malformed.js", 500, 'The page cannot be displayed because an internal server error has occurred.'),
]);
1 change: 1 addition & 0 deletions test/functional/www/143_skip_iis_custom_errors/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
IIS Custom Error page still gets served for non iisnode error responses
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var http = require('http');

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Hello, world!');
}).listen(process.env.PORT);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var http = require('http');

http.createServer(function (req, res) {
res.writeHead(400, {'Content-Type': 'text/html'});
res.end('Created error');
}).listen(process.env.PORT);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid js file
5 changes: 5 additions & 0 deletions test/functional/www/143_skip_iis_custom_errors/off/web.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<configuration>
<system.webServer>
<iisnode skipIISCustomErrors="false" />
</system.webServer>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var http = require('http');

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Hello, world!');
}).listen(process.env.PORT);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var http = require('http');

http.createServer(function (req, res) {
res.writeHead(400, {'Content-Type': 'text/html'});
res.end('App created error. Gets replaced by IIS Custom error if skipIISCustomErrors="false"');
}).listen(process.env.PORT);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid js file
5 changes: 5 additions & 0 deletions test/functional/www/143_skip_iis_custom_errors/on/web.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<configuration>
<system.webServer>
<iisnode skipIISCustomErrors="true" />
</system.webServer>
</configuration>
60 changes: 60 additions & 0 deletions test/functional/www/143_skip_iis_custom_errors/web.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<configuration>
<system.webServer>
<!--
errorMode="Custom" to prevent IIS from helpfully serving a more detailed error .aspx page when run locally
Default error mode is DetailedLocalOnly, which serves custom errors to remote users and more detailed error pages
to local users.
-->
<httpErrors existingResponse="Auto" errorMode="Custom">
<clear />
<remove statusCode="401" />
<error statusCode="401" path="error.html" responseMode="File" />
</httpErrors>
<handlers>
<add name="iisnode" path="*/*.js" verb="*" modules="iisnode" />
</handlers>
<iisnode devErrorsEnabled="false" />
</system.webServer>

<!--
Cause requests to hello.js to fail authentication before hitting iisnode.
In this scenario, we may want to re-route to a custom error page via IIS
since we won't be able to handle the request in the node application.
-->
<location path="off/access_denied.js">
<system.webServer>
<security>
<authorization>
<clear />
<add accessType="Deny" users="*" />
</authorization>
</security>
</system.webServer>
</location>
<location path="on/access_denied.js">
<system.webServer>
<security>
<authorization>
<clear />
<add accessType="Deny" users="*" />
</authorization>
</security>
</system.webServer>
</location>

<!--
Need to allow users access to error.html.
NOTE: Seprately, you will need to give IUSR account read access to custom error page so that IIS can access and serve the file.
-->
<location path="error.html">
<system.webServer>
<security>
<authorization>
<clear />
<add accessType="Allow" users="*" />
</authorization>
</security>
</system.webServer>
</location>

</configuration>