65.9K
CodeProject 正在变化。 阅读更多。
Home

数据:URL 的异步可插入协议处理程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (18投票s)

2006 年 1 月 28 日

CPOL

5分钟阅读

viewsIcon

167542

downloadIcon

2014

本文档描述了一种异步可插入协议实现,用于在 Internet Explorer 中支持 RFC 2397 中描述的 data: 协议。

Sample Image - DataProtocol.gif

引言

我第一次遇到 data: 协议是在看到 JavaScript Draw 网站时,这是一个用于涂鸦应用程序的 AJAX 实现。该网站的问题在于它在 Internet Explorer 中无法正常工作。通过查看源代码,我发现它之所以无法工作的原因之一是它使用了 data: URL 作为动态创建图像的源,而 Internet Explorer 不支持这一点。

data: 协议在 RFC 2397 中有描述。目前,唯一支持 data: 协议的浏览器是 Opera 和 Mozilla Firefox。本文档描述了一种 异步可插入协议 实现,用于在 Internet Explorer 中支持 data: 协议。data: 协议的一个可能用途是将小型图像嵌入到 HTML 中,以避免服务器请求。它也可以在 AJAX 应用程序(如 JavaScript Draw)中用于将 base64 编码的图像作为响应文本返回。

data: URL 格式

根据 RFC 的描述,协议本身非常简单。其格式为:

    dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
    mediatype  := [ type "/" subtype ] *( ";" parameter )
    data       := *urlchar
    parameter  := attribute "=" value

媒体类型指示了数据的类型及其编码。默认媒体类型为 text\plain;charset=US-ASCII。对于图像,媒体类型可以是 image/gifimage/png 等。URL 中可选的 base64 部分表示 URL 中表示的实际数据是以 base64 格式编码的。尽管 base64 编码在 URL 中的主要用途是用于二进制数据,但它也可以用于编码文本。最后,URL 的数据部分是 URL 所表示的实际编码数据。

接下来的部分将描述该协议是如何实现的。

解析 URL

ATL 的正则表达式类在解析 data: URL 时非常方便。以下是解析 URL 的正则表达式:

data:{(.*?/.*?)}?(;{.*?}={.*?})?{;(base64)?}?,{.*}

正则表达式将 URL 的各个部分捕获到五个不同的组中:

捕捉
0

媒体类型或 MIME 类型的 type/subtype 部分

1

与 MIME 类型一起指定的任何附加 parameterattribute 名称

2

组 1 中捕获的 attribute 的 value
3 base64 字符串
4 实际的 data 字符串

捕获 URL 的不同部分后,Base64 编码的数据将被转换为字节。ATL 函数 Base64Decode 用于此目的。

    int nReqLen = Base64DecodeGetRequiredLength(strData.GetLength());
    m_pvData = new BYTE[nReqLen]; 
    int nDestLen = nReqLen; 
    bRet = Base64Decode(strData, strData.GetLength(), m_pvData, &nDestLen) != 0; 
    m_dwDataLength = nDestLen;

将文本数据转换为 Unicode

如果数据格式是文本,则会将文本转换为 Unicode,以便 Internet Explorer 可以正确处理。源数据的编码来自媒体类型 parameter 部分中指定的 charset 属性。此类 URL 的一个示例是 data:text/plain;charset=iso-8859-8-i,%f9%ec%e5%ed - 这是用 ISO-8859-8-i 编码的一些希伯来文本。

要将多字节字符转换为 Unicode,我们需要使用著名的 MultiByteToWideChar 函数。MultiByteToWideChar 函数需要一个 DWORD 代码页标识符。经过一点研究,我们发现 MLang API 中的 IMultiLanguage 接口可用于从命名字符集获取代码页标识符。

CComPtr<IMultiLanguage2> spMLang;
if (SUCCEEDED(hr = spMLang.CoCreateInstance(CLSID_CMultiLanguage)))
{ MIMECSETINFO mi;
 if (SUCCEEDED(hr = spMLang->GetCharsetInfo(CComBSTR(GetCharset()), &mi)))
 ...
}

MIMECSETINFO 结构声明如下:

typedef struct tagMIMECSETINFO { 
 UINT uiCodePage; 
 UINT uiInternetEncoding; 
 WCHAR wszCharset[MAX_MIMECSET_NAME]; 
} MIMECSETINFO, *PMIMECSETINFO;

乍一看,似乎 uiCodePage 成员将提供所需代码页标识符;然而,根据我的经验,在某些情况下,uiCodePage 成员是所需代码页,而在其他情况下,uiInternetEncoding 是所需值。不幸的是,我找不到任何文档描述何时使用哪个。因此,转换字符集的代码会显得有些混乱。

int nSrcLen = strData.GetLength();
UINT uCodePage = mi.uiInternetEncoding;
int nWideChar = MultiByteToWideChar(uCodePage, 0, 
               (LPCSTR)strData, nSrcLen, NULL, 0);
if (nWideChar == 0)
{ 
    uCodePage = mi.uiCodePage;
    nWideChar = MultiByteToWideChar(uCodePage, 0, 
                (LPCSTR)strData, nSrcLen, NULL, 0);
}
if (nWideChar != 0)
{
    WCHAR* sz = new WCHAR[nWideChar + 1];
    MultiByteToWideChar(uCodePage, 0, 
                (LPCSTR)strData, nSrcLen, sz + 1, nWideChar);
    m_pvData = (BYTE*)sz;
    m_dwDataLength = (nWideChar + 1) * 2; 
    //If data is in Unicode it should have unicode lead bytes
    m_pvData[0] = 0xFF;
    m_pvData[1] = 0xFE;
}

一旦字符被转换为 Unicode 字节流,就需要将字节流的前面加上 Unicode 前导字节(0xFFFE),以指示 Internet Explorer。前导字节为 0xFFFE。

URL 解析为我们提供了数据和数据的 MIME 类型。可插入协议的实际实现非常简单。

实现异步可插入协议处理程序

异步可插入协议处理程序是一个实现 IInternetProtocolIInternetProtocolInfo 接口的 COM 对象。为了让 Internet Explorer 使用协议处理的 URL 方案,需要在 HKEY_CLASSES_ROOT\PROTOCOLS\Handler 下添加注册条目。以下是协议 COM 对象 .rgs 文件中的摘录:

HKCR
{ ...
    NoRemove PROTOCOLS
    {
        NoRemove Handler
        {
            ForceRemove data = s 'data: pluggable protocol'
            {
                val CLSID = s '{C79BF22F-25C4-4D3D-8183-14149EAB9C0C}'
            }
        }
    }
}

可插入协议处理程序实现中唯一有趣的方法是 IInternetProtocol::StartIInternetProtocol::ReadIInternetProtocol::Start 是由 Internet Explorer(实际上是 urlmon.dll)调用,通知处理程序需要从给定 URL 下载数据。可插入协议处理程序解析 URL 并下载数据。它使用 IInternetProtocolSink(调用者提供的回调接口)向调用者通知进度。调用者根据从协议处理程序收到的状态信息调用 IInternetProtocol::Read 来读取数据。data 协议处理程序的 start 方法实现如下:

STDMETHODIMP CDataPluggableProtocol::Start(
    LPCWSTR szUrl,
    IInternetProtocolSink *pIProtSink,
    IInternetBindInfo *pIBindInfo,
    DWORD grfSTI,
    DWORD dwReserved)
{
    HRESULT hr = S_OK;

    if (m_url.Parse(szUrl))
    {
        m_dwPos = 0;

        CAtlString strData = m_url.GetDataString();
            
        pIProtSink->ReportProgress(BINDSTATUS_FINDINGRESOURCE, strData);
        pIProtSink->ReportProgress(BINDSTATUS_CONNECTING, strData);
        pIProtSink->ReportProgress(BINDSTATUS_SENDINGREQUEST, strData);
        pIProtSink->ReportProgress(BINDSTATUS_VERIFIEDMIMETYPEAVAILABLE, 
                                      m_url.GetMimeType());
        pIProtSink->ReportData(BSCF_FIRSTDATANOTIFICATION, 0, 
                                  m_url.GetDataLength());
        pIProtSink->ReportData(BSCF_LASTDATANOTIFICATION | 
                                  BSCF_DATAFULLYAVAILABLE, 
                                  m_url.GetDataLength(), 
                                  m_url.GetDataLength());
        
    }
    else
    {
        if (grfSTI & PI_PARSE_URL)
            hr = S_FALSE;
    }

    return hr;
}

该函数解析 URL,从而自动提取数据。然后代码向调用者发送一系列通知。重要的调用是 ReportProgress(BINDSTATUS_VERIFIEDMIMETYPEAVAILABLE, m_url.GetMimeType());,它向调用者指示数据的 MIME 类型,以便调用者可以相应地处理数据。然后调用者调用 IInternetProtocol::Read 来读取数据。

测试协议处理程序

当项目构建时,协议处理程序会自动注册。一旦处理程序注册,data: URL 将在 Internet Explorer 中开始工作。该协议处理程序已在 mozilla.com 测试网站的 data: URL Tests 上进行了测试。处理程序通过了所有测试,除了一项。该测试失败是由于 Internet Explorer URL 长度的限制。到目前为止,尚未发现任何安全问题。我欢迎读者指出协议处理程序可能存在的任何安全问题。

历史

  • 2006 年 1 月 28 日 - 初始发布。
© . All rights reserved.