There are many ways to redirect HTTP traffic to HTTPS in IIS, and using ISAPI Filter is one of them. It has the advantage of being programmable, though it's considered a legacy technology nowadays. Use it if you feel appropriate.

In this article, we'll code an ISAPI Filter using C/C++ which

  • redirects user to HTTPS if not already,
  • while keeping its original pathname (/path/to/doc.html) and search (?a=b); hash (#abc) will be kept by browser;
  • and allows customization of redirect code and message, e.g. 301 Moved Permanently, 302 Found, etc.

Add ISAPI Filter

First, let's go through the steps to add an ISAPI Filter. We use InetMgr.exe:

InetMgr - ISAPI Filters

InetMgr - ISAPI Filters - Add ISAPI Filter

The executable to be added will get compiled later.

If ISAPI Filters is not available, make sure it's enabled in server roles and features:

Add Roles and Features Wizard - ISAPI Filters

Code step by step

Full code is attached afterwards.

  1. Create the project and basic code structure

    Refer to Developing and Debugging ISAPI Filters.

  2. Include needed headers

    #include <stdio.h>
    #include <windows.h>
    #include <httpfilt.h>
    
  3. Configure our filter to receive SF_NOTIFY_PREPROC_HEADERS event notification

    Add pVer->dwFlags = SF_NOTIFY_PREPROC_HEADERS; to GetFilterVersion function.

    Doc: GetFilterVersion Function, HTTP_FILTER_VERSION Structure (IIS)

    BOOL WINAPI GetFilterVersion(
        PHTTP_FILTER_VERSION pVer
    )
    {
      pVer->dwFlags = SF_NOTIFY_PREPROC_HEADERS;
    
      // This function must return TRUE for your filter to remain loaded and working properly.
      return TRUE;
    }
    
  4. Respond to SF_NOTIFY_PREPROC_HEADERS event notification in HttpFilterProc function

    Doc: HttpFilterProc Function

    DWORD WINAPI HttpFilterProc(
        PHTTP_FILTER_CONTEXT pfc,
        DWORD notificationType,
        LPVOID pvNotification
    )
    {
      // Check the notification type as all types of notification get through HttpFilterProc
      if (notificationType == SF_NOTIFY_PREPROC_HEADERS)
      {
        // Do our work, explained later
      }
    
      // The next filter in the notification chain should be called.
      return SF_STATUS_REQ_NEXT_NOTIFICATION;
    }
    
  5. Check whether the request is already served via HTTPS

    Doc: HTTP_FILTER_CONTEXT Structure (IIS)

    // sslRequired may be set via custom logic
    bool sslRequired = true;
    // If SSL is not required or the request is already served via SSL, just return
    if (!sslRequired || pfc->fIsSecurePort)
    {
      return SF_STATUS_REQ_NEXT_NOTIFICATION;
    }
    
  6. Get host and url of the current request

    so that we are able to only change protocol and keep other parts of URL untouched.

    HTTP_FILTER_PREPROC_HEADERS* headers = (HTTP_FILTER_PREPROC_HEADERS*) pvNotification;
    
    const DWORD maxHost = 256;
    DWORD nHost = maxHost;
    char host[maxHost];
    if (!headers->GetHeader(pfc, "Host:", host, &nHost))
    {
      return SF_STATUS_REQ_ERROR;
    }
    
    const DWORD maxURL = 2048;
    DWORD nURL = maxURL;
    char url[maxURL];
    if (!headers->GetHeader(pfc, "url", url, &nURL))
    {
      return SF_STATUS_REQ_ERROR;
    }
    
  7. Redirect to new location

    Doc: SF_REQ_SEND_RESPONSE_HEADER, SF_REQ_SEND_RESPONSE_HEADER callback function, HttpFilterProc Function

    const DWORD maxHeader = 4096;
    char newHeader[maxHeader];
    // Header format:
    // header1:value1\r\nheader2:value2\r\n\r\n
    if (sprintf_s(newHeader, maxHeader, "Location: https://%s%s\r\n\r\n", host, url) < 0)
    {
      return SF_STATUS_REQ_ERROR;
    }
    
    pfc->ServerSupportFunction(
          pfc,
          SF_REQ_SEND_RESPONSE_HEADER,
          (PVOID) "307 Temporary Redirect", // HTTP status string
          (DWORD) newHeader,                // response header
          0);                               // unused
    
    // The filter has handled the HTTP request.
    return SF_STATUS_REQ_FINISHED;
    

Final Code

#include <stdio.h>
#include <windows.h>
#include <httpfilt.h>

BOOL WINAPI GetFilterVersion(
    PHTTP_FILTER_VERSION pVer
)
{
  pVer->dwFlags = SF_NOTIFY_PREPROC_HEADERS;
  return TRUE;
}

DWORD WINAPI HttpFilterProc(
    PHTTP_FILTER_CONTEXT pfc,
    DWORD notificationType,
    LPVOID pvNotification
)
{
  if (notificationType == SF_NOTIFY_PREPROC_HEADERS)
  {
    bool sslRequired = true;
    if (!sslRequired || pfc->fIsSecurePort)
    {
      return SF_STATUS_REQ_NEXT_NOTIFICATION;
    }

    HTTP_FILTER_PREPROC_HEADERS* headers = (HTTP_FILTER_PREPROC_HEADERS*) pvNotification;

    const DWORD maxHost = 256;
    DWORD nHost = maxHost;
    char host[maxHost];
    if (!headers->GetHeader(pfc, "Host:", host, &nHost))
    {
      return SF_STATUS_REQ_ERROR;
    }

    const DWORD maxURL = 2048;
    DWORD nURL = maxURL;
    char url[maxURL];
    if (!headers->GetHeader(pfc, "url", url, &nURL))
    {
      return SF_STATUS_REQ_ERROR;
    }

    const DWORD maxHeader = 4096;
    char newHeader[maxHeader];
    if (sprintf_s(newHeader, maxHeader, "Location: https://%s%s\r\n\r\n", host, url) < 0)
    {
      return SF_STATUS_REQ_ERROR;
    }

    pfc->ServerSupportFunction(
          pfc,
          SF_REQ_SEND_RESPONSE_HEADER,
          (PVOID) "307 Temporary Redirect",
          (DWORD) newHeader,
          0);
    return SF_STATUS_REQ_FINISHED;
  }

  return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

Deploy ISAPI Filter

Build the project, copy the DLL, and use it as the executable for ISAPI Filter (see Add ISAPI Filter).

Remember to deploy relevant VC++ redistributables you are targeting (Platform Toolset in your project general configuration) along with your ISAPI Filter DLL.

Refer to Developing and Debugging ISAPI Filters for troubleshooting.