IISAPI Filter to redirect to login page

Windows specific forum
User avatar
RichAlgeni
Addict
Addict
Posts: 914
Joined: Wed Sep 22, 2010 1:50 am
Location: Bradenton, FL

IISAPI Filter to redirect to login page

Post by RichAlgeni »

Going off of the assumption that there are an infinite (almost) amount of ways to write a program, this is an IISAPI Filter dll that redirects the user to the login web page, if they are not logged in, or are not in a test or user portal folder. Note that 'test' and 'user portal' are names I use, and NOT preset. Also, I am going off of the assumption that it is better to minimize the use of Filters in IIS, and every page calls them at least once! So, the actual login is handled by an on demand IIS extension, and not the filter. When the dll is loaded, we tell IIS to run the function HttpFilterProc() (known as notifications) when IIS preprocesses the page headers, when IIS maps a request to a physical location on disk, and when IIS sends the response. We utilize the IIS variable *pfc\pFilterContext, as defined the in the Header include file, which allows us to pass context from one notification to another. It takes three notifications, or context calls, to force the user to go to the login page. Please also note that there may be better ways to do this, but this works the way I need it to. When better ways are found, I will update the post.

Code: Select all

; ------------------------------------------------------------
; Program name: ots_iisapi_filter.pb
;
; Purpose: 64 bit Internet Information Server Filter API Dll
; ------------------------------------------------------------

EnableExplicit

; set the constants needed to start

#ProgramName    = "ots_iisapi_filter"
#ProgramTitle   = "Orion TEK Solutions - 64 bit Unicode Internet Information Server Filter API Dll"
#ProgramVersion = "1.0.01c." + #PB_Editor_BuildCount

; add the program include files we need

XIncludeFile "httpFilt_unicode.pbi"

CompilerIf #PB_Compiler_OS = #PB_OS_Windows
    #systemSlash = "\"
CompilerElse
    #systemSlash = "/"
CompilerEndIf

; ************************************************************************************
; Procedure to write logging data to Mark Russinovich's Debug View
; https://docs.microsoft.com/en-us/sysinternals/downloads/debugview
; ************************************************************************************

Procedure.i WriteToLog(*logText)

    Protected logText.s = PeekS(*logText)

    OutputDebugString_(logText)

    ProcedureReturn #True

EndProcedure

; ************************************************************************************
; initialization procedure
; ************************************************************************************

ProcedureDLL.i AttachProcess(instance.i)

    Protected logText.s = #ProgramName + ", Ver: " + #ProgramVersion + " AttachProcess() > completed successfully"
    WriteToLog(@logText)

    ProcedureReturn #True

EndProcedure

; ************************************************************************************
; DetachProcess
; ************************************************************************************

ProcedureDLL.i DetachProcess(instance.i)
EndProcedure

; ************************************************************************************
; AttachThread
; ************************************************************************************

ProcedureDLL.i AttachThread(instance.i)
EndProcedure

; ************************************************************************************
; DetachThread
; ************************************************************************************

ProcedureDLL.i DetachThread(instance.i)
EndProcedure

; ************************************************************************************
; GetFilterVersion is called when the dll is loaded
; ************************************************************************************

ProcedureDLL.i GetFilterVersion(*pVer.HTTP_FILTER_VERSION)

    Protected logText.s
    Protected descriptionSize.l
    Protected notificationMask.l
    Protected description.s = "ISAPI Filter written in PureBasic, Ver: " + #ProgramVersion
    Protected netSource.s   = #ProgramName + ", Ver: " + #ProgramVersion + " GetFilterVersion()"

    notificationMask = #SF_NOTIFY_PREPROC_HEADERS | #SF_NOTIFY_URL_MAP | #SF_NOTIFY_SEND_RESPONSE
    descriptionSize  = StringByteLength(description, #PB_UTF8)

; make the length of the description the same number of characters as #SF_MAX_FILTER_DESC_LEN

    PokeL(@*pVer\dwFilterVersion, #HTTP_FILTER_REVISION)
    PokeS(@*pVer\lpszFilterDesc,  description, descriptionSize, #PB_UTF8)
    PokeL(@*pVer\dwFlags,         notificationMask)

    logText = netSource + ", ISAPI Server Version: 0x" + RSet(Hex(*pVer\dwServerFilterVersion, #PB_Long), 8, "0")
    logText + ", ISAPI Client Version: 0x" + RSet(Hex(*pVer\dwFilterVersion, #PB_Long), 8, "0")
    logText + ", notifications requested: 0x" + RSet(Hex(notificationMask, #PB_Long),  8, "0")

    WriteToLog(@logText)

; return true, returning false will cause the dll to be unloaded

    ProcedureReturn #True

EndProcedure

; ************************************************************************************
; This is the main function that is called for each client request pfc contains all needed data
; ************************************************************************************

ProcedureDLL.i HttpFilterProc(*pfc.HTTP_FILTER_CONTEXT, notificationType.l, *pvNotification)

; *pvNotification: Pointer to the notification-specific Structure that contains more information about the current context of the request.

    Protected pszURL.s
    Protected *referer
    Protected logText.s
    Protected referer.s
    Protected *lpszName
    Protected *referURL
    Protected foundOne.i
    Protected loggedIn.i
    Protected lpdwSize.i
    Protected lpszName.s
    Protected referURL.s
    Protected *lpvBuffer
    Protected headerVar.s
    Protected sessionID.s
    Protected cookieData.s
    Protected dwReserved.l
    Protected expireDtTm.i
    Protected lenPhysMap.l
    Protected lenReferer.i
    Protected requestURI.s
    Protected lenReferURL.i
    Protected requestPage.s
    Protected dbExpireDate.s
    Protected keepChecking.i
    Protected variableSize.i
    Protected filterContext.i
    Protected pageExtension.s
    Protected processReturn.i
    Protected sessionString.s
    Protected sessionLocator.i
    Protected physMapLocation.s
    Protected lenSessionString.i
    Protected sessionTerminator.i
    Protected initialSize.i   = 1024
    Protected netSource.s     = #ProgramName + ", Ver: " + #ProgramVersion + " HttpFilterProc()"

    Protected *pURLMap.HTTP_FILTER_URL_MAP
    Protected *pRawData.HTTP_FILTER_RAW_DATA
    Protected *pProcHead.HTTP_FILTER_PREPROC_HEADERS

; filterContext: = 0 is the flag to signal later calls to this filter that it needs to send the login page
; filterContext: = 1 is the flag to signal later calls to this filter that it needs to add a referer header
; filterContext: = 2 is not yet used
; filterContext: = 3 is not yet used, etc.

    logText = netSource + " , " + Str(notificationType)

; default return

    processReturn = #SF_STATUS_REQ_NEXT_NOTIFICATION

; for server variables and header data, see https://docs.microsoft.com/en-us/iis/web-dev-reference/server-variables

; now let's see what notification type we've received

    Select notificationType
    Case #SF_NOTIFY_PREPROC_HEADERS

; use distinct process number for each notification type

        logText       + " #SF_NOTIFY_PREPROC_HEADERS"

        *lpvBuffer    = AllocateMemory(initialSize)
        *lpszName     = AllocateMemory(64)

        lpszName      = "HEADER_Cookie"
        lpdwSize      = initialSize
        headerVar     = Space(initialSize)
        variableSize  = StringByteLength(lpszName, #PB_UTF8)
        PokeS(*lpszName, lpszName, variableSize, #PB_UTF8)
        If *pfc\GetServerVariable(*pfc.HTTP_FILTER_CONTEXT, *lpszName, *lpvBuffer, @lpdwSize)
            headerVar = PeekS(*lpvBuffer, lpdwSize, #PB_UTF8)
            headerVar = Trim(headerVar)
        Else
            headerVar = ""
        EndIf
        cookieData    = headerVar
        logText       + ", " + lpszName + " = '" + headerVar + "'"

        lpszName      = "REQUEST_URI"
        lpdwSize      = initialSize
        headerVar     = Space(initialSize)
        variableSize  = StringByteLength(lpszName, #PB_UTF8)
        PokeS(*lpszName, lpszName, variableSize, #PB_UTF8)
        If *pfc\GetServerVariable(*pfc.HTTP_FILTER_CONTEXT, *lpszName, *lpvBuffer, @lpdwSize)
            headerVar = PeekS(*lpvBuffer, lpdwSize, #PB_UTF8)
            headerVar = Trim(headerVar)
            headerVar = StringField(headerVar, 1, "?")
            headerVar = Trim(headerVar)
        Else
            headerVar = ""
        EndIf
        requestURI    = LCase(headerVar)
        logText       + ", " + lpszName + " = '" + headerVar + "'"

        lpszName      = "QUERY_STRING"
        lpdwSize      = initialSize
        headerVar     = Space(initialSize)
        variableSize  = StringByteLength(lpszName, #PB_UTF8)
        PokeS(*lpszName, lpszName, variableSize, #PB_UTF8)
        If *pfc\GetServerVariable(*pfc.HTTP_FILTER_CONTEXT, *lpszName, *lpvBuffer, @lpdwSize)
            headerVar = PeekS(*lpvBuffer, lpdwSize, #PB_UTF8)
            headerVar = Trim(headerVar)
        Else
            headerVar = ""
        EndIf

        logText       + ", " + lpszName + " = '" + headerVar + "'"

        requestPage   = GetFilePart(requestURI)
        pageExtension = GetExtensionPart(requestURI)

        loggedIn      = #False
        keepChecking  = #True

; release the memory we have allocated

        FreeMemory(*lpvBuffer)
        FreeMemory(*lpszName)

; if this uri isn't an htm or html file, it's not a web page, so there's no reason to force it to a login

        logText + ", pageExtension = " + pageExtension

        If Right(pageExtension, 3) <> "htm" And Right(pageExtension, 4) <> "html"
            keepChecking = #False
            logText + ", keepChecking = " + Str(keepChecking)
        EndIf

; if this is a page from user_portal, we don't need to process it any further, this is where users can create their account

        If keepChecking
            If Left(requestURI, 13) = "/user_portal/"
                keepChecking = #False
                logText + ", requestURI = " + requestURI + ", keepChecking = " + Str(keepChecking)
            EndIf
        EndIf

; if this is a page from testing, we don't need to process it any further

        If keepChecking
            If Left(requestURI, 9) = "/testing/"
                keepChecking = #False
                logText + ", requestURI = " + requestURI + ", keepChecking = " + Str(keepChecking)
            EndIf
        EndIf

; if it is a web page, and it doesn't reside in the web portal, we need to see if the user is logged in by having a valid session id. We get the session ID from the cookie

; add your own code here for cookie checking!!!!
; add your own code here for cookie checking!!!!
; add your own code here for cookie checking!!!!
; add your own code here for cookie checking!!!!
; add your own code here for cookie checking!!!!

; if this session is not logged in, this is a web page, and the source is not the user_portal...
; we must force the session To login by setting the first two bits of the filter context variable To 1

        If keepChecking
            filterContext = 1

; then allocate memory inside iisapi to hold a string, it gets deallocated (freed) by IIS

            If *pfc\AllocMem(*pfc.HTTP_FILTER_CONTEXT, 2048, dwReserved)
                PokeI(@*pfc\pFilterContext, filterContext)
                logText + ", memory allocated, new pFilterContext = " + Str(filterContext)
            Else
                logText = "In #SF_NOTIFY_PREPROC_HEADERS unable to allocate memory"
                WriteToLog(@logText)
                filterContext = 0

; no need to continue further processing, since we cannot allocate memory

                processReturn = #SF_STATUS_REQ_HANDLED_NOTIFICATION
            EndIf
        EndIf

; notification for when map url is fired

    Case #SF_NOTIFY_URL_MAP

; use distinct process number for each notification type

        filterContext = PeekI(@*pfc\pFilterContext)

        logText       + " #SF_NOTIFY_URL_MAP, current pFilterContext = " + Str(filterContext)

        *pURLMap      = *pvNotification

        pszURL        = PeekS(@*pURLMap\pszURL, -1, #PB_UTF8)
        lenPhysMap    = PeekL(@*pURLMap\cbPathBuff)
        physMapLocation = PeekS(@*pURLMap\pszPhysicalPath, -1, #PB_UTF8)

; if we have previously set the context bit 0 to 1, map the url to a login page, the mapping is the physical hard drive!

        If filterContext = 1
            logText         + ", current pszURL = '" + pszURL + "'"

            physMapLocation = "d:\your_web_root\login.htm"
            lenPhysMap      = StringByteLength(physMapLocation, #PB_UTF8)

; poke in a remapped url, to force logging in

            PokeS(@*pURLMap\pszPhysicalPath, physMapLocation, lenPhysMap, #PB_UTF8)
            PokeL(@*pURLMap\cbPathBuff, lenPhysMap); length does not matter, IIS reads until it hits a null!

; update the filter context, and save the url for later when we will make it the referer

            PokeI(@*pfc\pFilterContext, filterContext)
            PokeS(@*pfc\pFilterContext + 8, pszURL, -1, #PB_UTF8)

            logText + ", mapped url to '" + physMapLocation + "', new pFilterContext = " + Str(filterContext)

; else, we can send #SF_STATUS_REQ_HANDLED_NOTIFICATION, which tell IIS to stop any further filter notifications for this uri

        Else
            processReturn = #SF_STATUS_REQ_HANDLED_NOTIFICATION
        EndIf

; notification for when send response is fired

    Case #SF_NOTIFY_SEND_RESPONSE

; use distinct process number for each notification type

        filterContext = PeekI(@*pfc\pFilterContext)

        logText       + " #SF_NOTIFY_SEND_RESPONSE, current pFilterContext = " + Str(filterContext)

; if we have previously set the context bit 1 to 1, map the url to a login page

        If filterContext = 1
            *pProcHead  = *pvNotification

            referURL    = PeekS(@*pfc\pFilterContext + 8, -1, #PB_UTF8)
            lenReferURL = StringByteLength(referURL, #PB_UTF8)
            *referURL   = AllocateMemory(lenReferURL + 1)
            PokeS(*referURL, referURL, lenReferURL, #PB_UTF8)

            referer     = "Referer:"
            lenReferer  = StringByteLength(referer, #PB_UTF8)
            *referer    = AllocateMemory(lenReferer + 1)
            PokeS(*referer, referer, lenReferer, #PB_UTF8)

            filterContext = 0

; see https://docs.microsoft.com/en-us/previous-versions/dd435718(v=msdn.10)

            If *pProcHead\AddHeader(*pfc.HTTP_FILTER_CONTEXT, *referer, *referURL)
                logText + ", added header '" + referer + referURL + "' successfully, new pFilterContext = " + Str(filterContext)
            Else
                logText = "In #SF_NOTIFY_SEND_RESPONSE *pProcHead\AddHeader() returned 0"
            EndIf

            PokeI(@*pfc\pFilterContext, filterContext)

            FreeMemory(*referURL)
            FreeMemory(*referer)
        EndIf

; no more processing is needed for this request

        processReturn = #SF_STATUS_REQ_HANDLED_NOTIFICATION

; we should never hit this code!!!

    Default
        logText + ", Unlogged notification type encountered: " + Str(notificationType)
    EndSelect

    logText + ", procedure terminating"
    WriteToLog(@logText)

; return whatever the next notification should be

    ProcedureReturn processReturn

EndProcedure

; ************************************************************************************
; TerminateFilter is called before unloading the dll.
; ************************************************************************************

ProcedureDLL.i TerminateFilter(dwFlags.l)

    Protected logText.s = #ProgramName + ", Ver: " + #ProgramVersion + " TerminateFilter > process terminating"

    WriteToLog(@logText)

    ProcedureReturn #True

EndProcedure

; IDE Options = PureBasic 5.73 LTS (Windows - x64)
; ExecutableFormat = Shared dll
; CursorPosition = 176
; FirstLine = 173
; Folding = --
; EnableThread
; UseIcon = ..\OTS\ots.ico
; Executable = ots_iisapi_filter.dll
; DisableDebugger
; HideErrorLog
; CurrentDirectory = \
; CompileSourceDirectory
; EnableCompileCount = 2
; EnableBuildCount = 2
; EnableExeConstant
; IncludeVersionInfo