使用 WinINet API 将文件上传到 IIS WebDav 目录

本文介绍如何使用 WinINet API 以编程方式将文件上传到 Microsoft Internet Information Services (IIS) 上托管的 WebDav 目录。

原始产品版本: Internet 信息服务
原始 KB 编号: 2001156

症状

应用程序使用 WinINet API 以编程方式将文件上传到 IIS 上托管的 WebDav 目录, (HTTP) PUT 谓词发送超文本传输协议。 你注意到,应用程序在 IIS 和 Windows Server 上正常工作,但可能无法将文件上传到 IIS WebDav 目录,即使没有对 WinINet 应用程序进行代码更改也是如此。

原因

此问题的原因是 IIS 上运行的 WebDav 的设计更改。 在 IIS 上运行的 WebDav 现在需要身份验证,如果仅使用匿名身份验证,则不起作用。 因此,使用 的 WinINet API 序列 的应用程序将遇到 对 HttpEndRequestHttpSendRequestExHttpEndRequestInternetWriteFile调用返回 FALSE,并将GetLastError()指示错误代码 12032 - 。 ERROR_INTERNET_FORCE_RETRY

解决方案

此问题的解决方法是重试相同的操作序列,即:

  1. HttpSendRequestEx
  2. InternetWriteFile
  3. HttpEndRequest

直到 HttpEndRequest 不返回 FALSEGetLastError() 不报告回 12032 (或) 出现其他错误。 如果 IIS 显示错误的身份验证信息,则 IIS 将在每次重试时继续返回 HTTP 错误 401。 因此,需要跟踪函数返回错误 12032 的次数 HttpEndRequest ,并防止进入无限循环。

如果有 Windows NTLM 身份验证, HttpEndRequest 将返回错误 12032 最多两次,以满足三向 NTLM 握手。 第一个错误 12032 将指示来自服务器的 HTTP 错误 401 响应,第二个错误 12032 将指示来自服务器的 Type-2 NTLM 握手消息,如果向 IIS 传递了有效的身份验证信息,则将对用户进行身份验证并成功上传。

使用重试逻辑在循环中调用上述函数时,请注意,对 的调用 InternetWriteFile 是多次进行的。 这意味着,对 InternetWriteFile 的调用最终将通过网络写入数据,这将导致带宽浪费。 若要防止这种情况发生,可以将虚拟 HTTP HEAD 请求发送到服务器,该请求将对请求进行预身份验证,并导致 稍后调用 HttpSendRequest 在调用 时 InternetWriteFile 不发送 HTTP 有效负载。 如果熟悉网络监视器或 WinINet 日志记录,你将看到发送到服务器的第 PUT 一个请求将具有零的内容长度,这将阻止有效负载传输,在 NTLM 握手完成之前不会传输有效负载。

WebDav 功能不要求你使用 Windows 身份验证;可以将 WebDav 服务器配置为使用基于 SSL 的基本身份验证,这将确保数据上传的安全性。 配置基本身份验证后,可以直接在传出请求中注入有效的 base-64 编码用户名密码字符串,这将防止 IIS 返回 HTTP 错误 401,这就是不会返回错误 12032 的原因 HttpEndRequest 。 可以通过调用 WinINet API 将基本身份验证信息添加到传出请求:

HttpAddRequestHeaders(hRequest, "Authorization: Basic <<valid base-64 encoded username:password string>>\r\n", -1, HTTP_ADDREQ_FLAG_ADD);

在调用 HttpSendRequestEx 以直接在传出 HTTP 请求中注入 Authorization 标头之前执行此操作。

以下代码示例演示如何使用重试逻辑来处理来自 HttpEndRequest的错误 12032 返回响应。 此示例不包括上面讨论的预身份验证请求。 若要预先进行身份验证,只需在调用以下代码之前HttpSendRequestExHttpOpenRequestHttpSendRequest使用 HTTP HEAD 谓词调用 目标服务器。

代码示例

BOOL UseHttpSendReqEx(HINTERNET hRequest, DWORD dwPostSize)
{
    INTERNET_BUFFERS BufferIn;
    DWORD dwBytesWritten;
    int iChunkCtr;
    BYTE pBuffer[1024];
    BOOL bRet;
    BufferIn.dwStructSize = sizeof( INTERNET_BUFFERS ); // Must be set or you will get an error
    BufferIn.Next = NULL;
    BufferIn.lpcszHeader = NULL;
    BufferIn.dwHeadersLength = 0;
    BufferIn.dwHeadersTotal = 0;
    BufferIn.lpvBuffer = NULL;
    BufferIn.dwBufferLength = 0;
    BufferIn.dwBufferTotal = dwPostSize; // This is the only member used other than dwStructSize
    BufferIn.dwOffsetLow = 0;
    BufferIn.dwOffsetHigh = 0;
    //  The following variable will keep track of the number of times HttpSendRequestEx is called
    int iNumTrials = 0;
    bool bRetVal = FALSE;
    //  The retry goto is to re-try the operation when HttpEndRequest returns error 12032.
    while(1)
    {
        if(!HttpSendRequestEx( hRequest, &BufferIn, NULL, 0, 0))
        {
            printf( "Error on HttpSendRequestEx %d\n",GetLastError());
            return FALSE;
        }
        FillMemory(pBuffer, 1024, 'D'); // Fill buffer with data
        bRet=TRUE;
        for(iChunkCtr=1; iChunkCtr<=(int)dwPostSize/1024 && bRet; iChunkCtr++)
        {
            dwBytesWritten = 0;
            if(bRet=InternetWriteFile( hRequest, pBuffer, 1024, &dwBytesWritten))
                printf( "\r%d bytes sent.", iChunkCtr*1024);
        }
        if(!bRet)
        {
            printf( "\nError on InternetWriteFile %lu\n",GetLastError());
            return FALSE;
        }
        if(!HttpEndRequest(hRequest, NULL, 0, 0))
        {
            int iLastError = GetLastError();
            printf( "Error on HttpEndRequest %lu \n", iLastError);
            //  Use the following logic to "retry" after receiving error 12032 from HttpEndRequest
            //
            //  Error 12032 = ERROR_INTERNET_FORCE_RETRY means that you just need to send the request again
            //
            //  Sending request again means that you simply need to call:
            //
            //  HttpSendRequest, InternetWriteFile, HttpEndRequest until HttpEndRequest does not return
            //  back error 12032.
            //
            //  Since NTLM is a 3-way handshake protocol, it will happen that HttpEndRequest will return
            //  error 12032 two times and hence the following check.
            //
            //  If error 12032 is returned 3 or more number of times, then there is some Other error.
            if(iLastError == 12032 && iNumTrials < 3) {
                iNumTrials++;
                continue;   // This will retry HttpSendRequestEx...
            }
            return FALSE;
        }
        return TRUE;
    }
}

更多信息