用ISAPI Filter設置HttpOnly屬性

作者:Tony Qu

說到ISAPI很多人會覺得很陌生,因爲如果你是做ASP.NET開發的話,ISAPI的方式已經過時,取而代之的是HttpHandler和HttpModule,說到這兩個東西很多人估計明白了,ISAPI可以說是早期實現請求攔截和處理的唯一途徑,只是隨着ASP.NET的流行,漸漸淡出了開發人員的視野。

 

此文的開發場景是這樣的,我們公司使用古老的ASP語言,但是ASP的Response.Cookies屬性中沒有HttpOnly(但.Net的Cookie對象是有HttpOnly屬性的),有帖子說可以利用Path屬性來設置HttpOnly,可以這麼做是因爲我們在頁面中設置cookie值的動作都會被轉換成Set-Cookie頭,如下

Set-Cookie: user=t=bfabf0b1c1133a822; path=/
但如果要讓cookie變成HttpOnly,就需要用如下格式:
Set-Cookie: user=t=bfabf0b1c1133a822; path=/;HttpOnly

理論上講設置Path是完全可行的,因爲說白了就是在原來Path的基礎上增加;HttpOnly,

但經試驗表明這行不通,比如我用下面的代碼

Response.Cookies(“user”).Path+=”;HttpOnly”;

得到的結果卻是

Set-Cookie: user=t=bfabf0b1c1133a822; path=/3B%;HttpOnly

這顯然是不行的,所以我們不得不考慮用ISAPI來實現。

 

ISAPI基礎

首先,請不要把ISAPI Extension和ISAPI Filter混爲一談,這兩個東西雖然只差一個字,但卻完全是兩樣東西,所提供的接口是完全不一樣的。ISAPI Extension是一個類似頁面的dll,你可以對它做post或get提交,如http://localhost/abc.dll?a=1,從嚴格意義上講它沒有攔截的功能,和cgi差不多。而ISAPI Filter則是具有過濾功能的,你可以在IIS網站的屬性中添加需要加載的ISAPI Filter,例如asp.net的實現也使用了一個ISAPI Filter,叫做aspnet_filter.dll。

ISAPI Filter說到底就是一個DLL,它有兩個主要的接口:GetFilterVersion和HttpFilterProc,如下所示:

BOOL WINAPI __stdcall GetFilterVersion(HTTP_FILTER_VERSION *pVer) 
{ 
 /* Specify the types and order of notification */ 
 
 pVer->dwFlags = (SF_NOTIFY_PREPROC_HEADERS | SF_NOTIFY_AUTHENTICATION | 
 SF_NOTIFY_URL_MAP | SF_NOTIFY_SEND_RAW_DATA | SF_NOTIFY_LOG | SF_NOTIFY_END_OF_NET_SESSION ); 
 
 pVer->dwFilterVersion = HTTP_FILTER_REVISION; 
 
 strcpy(pVer->lpszFilterDesc, "Upper case conversion filter, Version 1.0"); 
 
 CFile myFile("c:\\mylist.html", CFile::modeCreate | CFile::modeWrite);
 myFile.SeekToEnd();
 char myText[40];
 strcpy(myText,"<B>GetFilterVersion </B><BR><BR>");
 myFile.Write(myText,strlen(myText));
 myFile.Close();
 
 return TRUE; 
}

GetFilterVersion不僅僅是用來獲得Filter版本這麼簡單,它可以用來過濾需要觸發的事件,這些事件的詳細信息你可以參考http://msdn.microsoft.com/en-us/library/ms825957.aspx。請注意,這裏做的是或操作,而不是與操作,學過數理邏輯的應該明白這個是幹嘛用的,就是值的疊加,說的再直接點,EventA|EventB就是我既要Event A也要Event B。

DWORD WINAPI __stdcall HttpFilterProc(HTTP_FILTER_CONTEXT *pfc, DWORD NotificationType, VOID *pvData) 
{ 
 CFile myFile("c:\\mylist.html", CFile::modeWrite);
 myFile.SeekToEnd();
 
 switch (NotificationType) { 
 
 case SF_NOTIFY_ACCESS_DENIED : 
 
 myFile.Write("SF_NOTIFY_ACCESS_DENIED<BR>",strlen("SF_NOTIFY_ACCESS_DENIED<BR>"));
 break;
 
 case SF_NOTIFY_AUTH_COMPLETE :
 
 myFile.Write("SF_NOTIFY_AUTH_COMPLETE<BR>",strlen("SF_NOTIFY_AUTH_COMPLETE<BR>"));
 break;
 
 case SF_NOTIFY_AUTHENTICATION :
 
 myFile.Write("SF_NOTIFY_AUTHENTICATION<BR>",strlen("SF_NOTIFY_AUTHENTICATION<BR>"));
 break;
 
 case SF_NOTIFY_END_OF_NET_SESSION : 
 
 myFile.Write("SF_NOTIFY_END_OF_NET_SESSION<BR>",strlen("SF_NOTIFY_END_OF_NET_SESSION<BR>"));
 break;
 
 case SF_NOTIFY_END_OF_REQUEST : 
 
 myFile.Write("SF_NOTIFY_END_OF_REQUEST<BR>",strlen("SF_NOTIFY_END_OF_REQUEST<BR>"));
 break;
 
 case SF_NOTIFY_LOG : 
 
 myFile.Write("SF_NOTIFY_LOG<BR>",strlen("SF_NOTIFY_LOG<BR>"));
 break;
 
 case SF_NOTIFY_PREPROC_HEADERS : 
 
 myFile.Write("SF_NOTIFY_PREPROC_HEADERS<BR>",strlen("SF_NOTIFY_PREPROC_HEADERS<BR>"));
 break;
 
 case SF_NOTIFY_READ_RAW_DATA : 
 
 myFile.Write("SF_NOTIFY_READ_RAW_DATA<BR>",strlen("SF_NOTIFY_READ_RAW_DATA<BR>"));
 break;
 
 case SF_NOTIFY_SEND_RAW_DATA :
 
 myFile.Write("SF_NOTIFY_SEND_RAW_DATA<BR>",strlen("SF_NOTIFY_SEND_RAW_DATA<BR>"));
 break;
 
 case SF_NOTIFY_SEND_RESPONSE : 
 
 myFile.Write("SF_NOTIFY_SEND_RESPONSE<BR>",strlen("SF_NOTIFY_SEND_RESPONSE<BR>"));
 break;
 
 case SF_NOTIFY_URL_MAP : 
 
 myFile.Write("SF_NOTIFY_URL_MAP<BR>",strlen("SF_NOTIFY_URL_MAP<BR>"));
 break;
 
 case SF_NOTIFY_SECURE_PORT : 
 
 myFile.Write("SF_NOTIFY_SECURE_PORT<BR>",strlen("SF_NOTIFY_SECURE_PORT<BR>"));
 break;
 
 case SF_NOTIFY_NONSECURE_PORT :
 
 myFile.Write("SF_NOTIFY_NONSECURE_PORT<BR>",strlen("SF_NOTIFY_NONSECURE_PORT<BR>"));
 break;
 
 default : 
 break; 
 } 
 
 
 myFile.Close();
 
 return SF_STATUS_REQ_NEXT_NOTIFICATION; 
}

HttpFilterProc是主要入口,相當於Console程序中的main。上面這段代碼是在這些事件觸發時寫入一個日誌,這樣便於調試。

說到這裏我們來了解下通常開發一個ISAPI Filter的流程。

a. 獲得一個現有的ISAPI Filter項目,當做模板,這個網上很多,google一下就有了。

b. 修改GetFilterVersion中的dwFlags的值來決定需要哪些事件

c. 修改HttpFilterProc中的case分支,刪除不需要的事件

d. 在需要處理的事件中寫代碼。

 

有一件事必須提醒大家,在寫ISAPI時,你千萬不要忘了把這兩個接口暴露出去,也就是定義DLL的EXPORTS,如下:

LIBRARY "isapi_sample"
EXPORTS
HttpFilterProc
GetFilterVersion

 

事件的執行順序

在ASP.NET中我們有Page Life Cycle,ISAPI Filter也是如此,這些時間的執行順序可以在 http://msdn.microsoft.com/en-us/library/ms524855(VS.90).aspx 上找到,下面的事件就是按執行順序排列的。

SF_NOTIFY_READ_RAW_DATA

SF_NOTIFY_PREPROC_HEADERS

SF_NOTIFY_URL_MAP 

SF_NOTIFY_AUTHENTICATION

SF_NOTIFY_AUTH_COMPLETE

SF_NOTIFY_SEND_RESPONSE

SF_NOTIFY_SEND_RAW_DATA

SF_NOTIFY_END_OF_REQUEST

SF_NOTIFY_LOG

SF_NOTIFY_END_OF_NET_SESSION

通過分析,我們知道要想獲得Set-Cookie header必須在ASP把頁面處理完之後,因爲ASP頁面代碼有可能會設置Cookie值,所以SF_NOTIFY_PREPROC_HEADERS事件並不合適,因爲它是在收到請求後,處理頁面前觸發的,我們需要的是在頁面處理完,發送前觸發的事件,所以SF_NOTIFY_SEND_RESPONSE最合適。在下一節我們將講解如何在該事件中添加處理代碼。

 

如何遍歷Set-Cookie

HttpFilterProc函數的第三個參數VOID *pvData是對應事件的數據,爲了獲得header裏面的數據,我們會把它轉換成PHTTP_FILTER_PREPROC_HEADERS,因爲我們先要把Set-cookie的數據讀出來,然後才能處理。

代碼如下:

 1: case SF_NOTIFY_SEND_RESPONSE : 
 2: pPH = (PHTTP_FILTER_PREPROC_HEADERS)pvData;
 3: pPH->GetHeader(pfc, "Set-Cookie:", szBuffer, &dwSize);
 4:  
 5: cookieNum=sizeof(strtok(szBuffer,","));
 6: if(cookieNum>0)
 7: {
 8: //handle the cookies that are read from header
 9: ...
 10: }

這裏的szBuffer就是我們獲得的Set-Cookie的字符串,這裏要講一下Set-Cookie到底是啥,因爲很多程序員對Set-Cookie的含義和表示形式不是特別瞭解。

每次我們在頁面中設置Cookie值,無論是ASP還是ASP.NET,都會把設置的操作轉換爲Set-Cookie中的一段字符串,如果你使用Fiddler或者HttpFox跟蹤這些請求的話,你會發現頭裏面有一項就是Set-Cookie項,這項僅在有設置Cookie的操作時纔會有。另外,Set-Cookie中的每一個Cookie字符串使用逗號分隔開的,如下

Set-Cookie: test1=a; path=/, test2=b; path=/

這裏設置了名爲test1和test2的兩個cookie值,單個cookie的屬性之間使用分號分隔的。也正是因爲如此,這段代碼中使用strtok來獲得字符串中每一段用逗號分隔的cookie字符串,這裏的cookieNum表示Set-cookie中cookie字符串的總數(注意,不是字符的總數)。

一旦我們獲得了每一個cookie的字符串,我們就可以把;HttpOnly附加到這些字符串的最後,並最終把字符串拼起來組成Set-Cookie字符串,關於如何做字符串拼接本文就不多講了,這完全是C++實現的問題。

 

如何覆蓋Set-Cookie字符串

這裏的設置cookie和我們平時在代碼裏做的可不太一樣,因爲我們要直接修改請求中的Set-Cookie,之所以是修改而不是增加新的Set-Cookie,是因爲Set-Cookie在請求的header中只能有一個,

HTTP_FILTER_SEND_RESPONSE * pResponse=(HTTP_FILTER_SEND_RESPONSE *)pvData;
BOOL fServer = TRUE;
fServer = pResponse->SetHeader(pfc, "Set-Cookie:",szHeader);

上面的代碼把pvData轉換成HTTP_FILTER_SEND_RESPONSE類型,這樣我們就可以對Response進行操作,並通過調用它的SetHeader方法來設置Set-Cookie header。

 

完整代碼下載:isapi_sample.zip (VC6項目),最主要的是MyISAPI.cpp和MyISAPI.def文件,其他都是工程文件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章