评论

收藏

[通信技术] 基于SSL(TLS)的HTTPS网页下载——如何编写健壮的可靠的网页下载

网络安全 网络安全 发布于:2021-07-01 17:09 | 阅读数:418 | 评论:0

  源码下载地址
案例开发环境:VS2010
本案例未使用openssl库,内部提供了sslite.dll库进行TLS会话,该库提供了ISSLSession接口用于建立SSL会话。下载的是网易(www.163.com)的主页。程序执行后会打印SSL会话的加密套件名称和Http响应头,并在C盘根目录下输出“TestSSLHttp.html”和“TestSSLHttp_body.html”两个文件。前者是服务器响应的原始文件即包含了响应头,后者是响应数据文件(本案例中为主页HTML)。
DSC0000.png

  HTTP协议很简单,写个简单的socket程序通过GET命令就能把网页给down下来。但接收大的网络资源就复杂多了。何时解析、如何解析完整的HTTP响应头,就是个头疼问题。因为你不能指望一次recv就能接收完所有响应数据,也不能指望服务器先发送完HTTP响应头,然后再发送响应数据(有可能是两者一并发送的)。只有把HTTP响应头彻底解析了,我们才能知道后续接收的Body数据有多大,何时才能接收完毕。
  比如通过响应头的"Content-Length"字段,才能知道后续Body的大小。这个大小可能超过了你之前开辟的接收数据缓存区大小。当然你可以在得知Body大小后,重新开辟一个与"Content-Length"一样大小的缓存区。但这样做显然是不明智的,比如你get的是一部4K高清蓝光小电影,蓝光电影不一定能get到,蓝屏电脑倒有可能get到。。。。。。
  遇到服务器明确给出"Content-Length"字段,是一件值得额手称庆的大喜事,但不是每个IT民工都这么幸运。如果遇到的是不靠谱的服务器,发送的是"Transfer-Encoding: chunked",那你就必须锻炼自己真正的解析和组织能力了。这些分块传输的数据,显然不会以你接收的节奏到达你的缓冲区,比如先接收到一个block块大小,然后是一个完整的块数据,很有可能你会接收到多个块或者不完整的块,这就需要你站在宏观的角度把他们拼接起来。
  如果你遇到的是甩的一米的服务器,它不仅给你的是chunked,而且还增加了"Content-Encoding: gzip",那么你就需要拼接后进行解压,当然你也可能遇到的是"deflate"压缩。
附:我写过web服务器,所以也知道服务器的心理。。。。。。
HttpServer:一款Windows平台下基于IOCP模型的高并发轻量级web服务器
  题外话:我一直困惑的是HTTP协议为何不是对分块数据单独gzip压缩然后传输,而只能是整体gzip压缩后再分块传输。这个对大资源传输很关键,比如上面的4K高清蓝光小电影,显然不能通过gzip+chunked方式传输,土豪服务器例外。
  当然你也可以用开源的llhttp来解析收到的http数据,从而避免上述可能会遇到的各种坑。最新版本的nodejs中就使用llhttp代替之前的的http-parser,据说解析效率有大幅提升。为此我下载了nodejs源码,并编译了一把,这是一个快乐的过程,因为你可以看到v8引擎,openssl,zlib等各种开源库。。。。,不过llhttp只负责解析,不负责缓存,因此你还是需要在解析的过程中,进行数据缓存。
关于V8引擎的使用参见文章
V8引擎静态库及其调用方法
  以下是sslite库提供的接口,SSLConnect是建立连接,SSLHandShake是SSL握手,握手成功后即可调用SSLSend和SSLRecv进行数据接收和发送,非常简单。如果接收数据很多,SSLRecv会通过回调函数将数据抛给调用层。
  以下是部分源码截图,注释很多,就不一一解释了。
#define END_RESPONSE_HEADER    "\r\n\r\n"
#define CRLF          "\r\n"
// 用于保存http响应的解析的相关参数
#define MAX_RESPONSE_HEADER_LEN    8196    // 响应头最大为8K
typedef struct http_params_st{
  BOOL bHeaderComplete;                // 响应头数据是否接收完毕
  BOOL bMessageComplete;                // 响应数据是否接收完毕
  BOOL bChunked;                    // 传输方式是否为分块传输
  int  iStatusCode;                  // HTTP响应码
  __int64 i64TotalReaded;                // 一共读取的数据
  __int64 i64ContentLen;                // Content-Length长度(响应头中解析出的"Content-Length"字段)
  __int64 i64BodyLen;                  // 实际的body数据长度
                                  
  char szResponseHeader[MAX_RESPONSE_HEADER_LEN];    // 缓存HTTP响应头
  int iResponseHeaderLen;                // 响应头的长度
  BOOL bResponseParsed;                // 响应头是否已解析
  HANDLE hFile;                    // 文件句柄,用于保存接收到的所有响应数据(原始数据)
  HANDLE hFileBody;                  // 文件句柄,仅保存body数据
  map<string, string>  mapHeader;            // 响应头中key=value对
  http_params_st(){
    iStatusCode = 0;
    bHeaderComplete = FALSE;
    bMessageComplete = FALSE;
    bChunked = FALSE;
    i64TotalReaded = 0;
    i64ContentLen = 0;
    i64BodyLen = 0;
    memset(szResponseHeader, 0, MAX_RESPONSE_HEADER_LEN);
    bResponseParsed = FALSE;
    iResponseHeaderLen = 0;
    hFile = NULL;
    hFileBody = NULL;
  }
}HTTP_PARAMS;
// 字符串去除头尾的空格
extern void StrTrim(char* pszSrc);
// 解析HTTP 响应头
extern BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams);
// 根据关键字获取对应的值
extern BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue);

//=============================以下llhttp的回调函数=============================
// HTTP响应头读取完毕
static int on_llhttp_headers_complete(llhttp_t* llhttp)
{
  HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  pHttpParams->bHeaderComplete = TRUE;
  
  return HPE_OK;
}
// HTTP响应读取完毕
static int on_llhttp_message_complete(llhttp_t* llhttp)
{
  HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  pHttpParams->bMessageComplete = TRUE;
  return HPE_OK;
}
// llhttp上抛的body数据
static int on_llhttp_body(llhttp_t* llhttp, const char *at, size_t length)
{
  HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  
  pHttpParams->i64BodyLen += length;
  if(INVALID_HANDLE_VALUE != pHttpParams->hFileBody && NULL != pHttpParams->hFileBody)
  {
    DWORD dwWrited = 0;
    ::WriteFile(pHttpParams->hFileBody, at, length, &dwWrited, NULL);
  }
  return HPE_OK;
}

//=============================以下为SSL层返回的业务数据=============================
static int OnSSLHttpDataNotify(const BYTE* pData, int iDataLen, DWORD dwCallbackData1, DWORD dwCallbackData2)
{
  if(NULL == pData || iDataLen <= 0)
    return SSL_DATA_RECV_FAILED;
  llhttp_t* llhttp = (llhttp_t*)dwCallbackData1;        // 来自SSL通信的用户自定义数据,此案例中为llhttp解析器
  HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;    // 来自llhttp的用户自定义数据
  pHttpParams->i64TotalReaded += iDataLen;          // 计算一共读取的数据
  // 将接收到的数据写入文件,这是原始数据,包含响应头
  // 数据内容可能是chunked,因此需要进一步解析
  DWORD dwWrited = 0;
  ::WriteFile(pHttpParams->hFile, pData, iDataLen, &dwWrited, NULL);

  // 调用llhttp进行解析
  int iRet = llhttp_execute(llhttp, (const char*)pData, iDataLen);
  if(HPE_OK != iRet)
    return SSL_DATA_RECV_FAILED;    // 通知SSL层:业务层发生错误,SSLRecv函数将返回    
  // 将数据缓存到pHttpParams->szResponseHeader
  if(0 == pHttpParams->iResponseHeaderLen)
  {
    
    if(pHttpParams->i64TotalReaded > MAX_RESPONSE_HEADER_LEN)
    {
      int iTotalReaded = int(pHttpParams->i64TotalReaded);
      int iPreReaded = iTotalReaded - iDataLen; // 之前读取的长度
      if(iPreReaded < MAX_RESPONSE_HEADER_LEN)
        memcpy(pHttpParams->szResponseHeader+iPreReaded, pData, MAX_RESPONSE_HEADER_LEN-iPreReaded);
      pHttpParams->iResponseHeaderLen = MAX_RESPONSE_HEADER_LEN;
    }
    else
    {
      int iTotalReaded = int(pHttpParams->i64TotalReaded);
      memcpy(pHttpParams->szResponseHeader+iTotalReaded-iDataLen, pData, iDataLen);
      pHttpParams->iResponseHeaderLen = iTotalReaded;
    }
  }
  
  // 计算HTTP响应头的长度
  if(!pHttpParams->bHeaderComplete)
  {
    // 缓冲区已满但没发现头,说明响应头太大超过8K,防止恶意***
    if(MAX_RESPONSE_HEADER_LEN == pHttpParams->iResponseHeaderLen)
    {
      printf("Too large HTTP response header.\r\n");
      return SSL_DATA_RECV_FAILED;
    }
  }
  else
  {
    // 如果没有解析HTTP响应头,则进行解析
    if(!pHttpParams->bResponseParsed)
    {
      // 查找"\r\n\r\n"
      char* pszResponseHeader = pHttpParams->szResponseHeader;
      char* pszFind = strstr(pszResponseHeader, END_RESPONSE_HEADER);  
      int iPos = pszFind - pszResponseHeader;
      pHttpParams->iResponseHeaderLen = iPos + 4;      // 计算真实的响应头长度,包含4字节的"\r\n\r\n"
      *(pszResponseHeader+pHttpParams->iResponseHeaderLen) = 0;
      pHttpParams->bResponseParsed = TRUE;
      pHttpParams->iStatusCode = llhttp->status_code;
      // 解析HTTP响应头
      ParseResponseHeader(pHttpParams);
      // 获取Content-Length长度
      string strValue;
      if(GetValueByKey(pHttpParams, "Content-Length", strValue))
      {
        pHttpParams->i64ContentLen = ::_atoi64(strValue.c_str());
      }
      else
      {
        pHttpParams->i64ContentLen = -1;  // 没有Content-Length字段
      }
      // 获取Transfer-Encoding编码方式,是否为chunked分块传输
      pHttpParams->bChunked = FALSE;
      if(GetValueByKey(pHttpParams, "Transfer-Encoding", strValue))
      {
        if(0 == _stricmp(strValue.c_str(), "chunked"))
          pHttpParams->bChunked = TRUE;
      }
      // HTTP response头中既没有Content-Length字段,也没有Chunked字段,因此无法明确后续内容大小
      if(pHttpParams->i64ContentLen < 0 && !pHttpParams->bChunked)
        return SSL_DATA_RECV_FAILED;
    }
  }
  // 业务层数据全部读取完毕
  if(pHttpParams->bMessageComplete)
  {
    // 关闭文件
    return SSL_DATA_RECV_FINISHED;    // 通知SSL层:数据接收完毕,SSLRecv函数将返回TRUE
  }
  return SSL_DATA_RECV_STILL;        // 通知SSL层:继续接收数据,SSLRecv函数将继续接收服务器数据
}
// HTTPS协议测试
int _tmain(int argc, _TCHAR* argv[])
{
  // 加载sslite.dll
  CSSLWrap sslWrap;
  if(!sslWrap.Load())
  {
    printf("Load sslite.dll failed!\r\n");
    return -1;
  }
  printf("Load sslite.dll successfully!\r\n");
  // 获取ISSLSession接口
  ISSLSession* pSSLSession = sslWrap.GetSSLSession();
  
  //const char* pszServer = "www.sina.com.cn";
  //const char* pszServer = "www.baidu.com";  
  const char* pszServer =  "www.163.com";    // chunked
  int iRet = 0;
  // 建立SSL会话,也可以调用SSLConnect后再调用SSLHandShake来实现SSL会话
  if(!pSSLSession->SSLEstablish(pszServer, 443, iRet))
  {
    if(SSL_RET_CONNECT == iRet)
    {
      printf("Connect %s failed!\r\n", pszServer);
    }
    else if(SSL_RET_HANDSHAKE == iRet)
    {
      printf("SSL handshake failed!\r\n");
    }
    return -1;
  }
  // 建立连接后,显示当前的加密套件名称和ECC(椭圆加密)的组名称
  printf("SSL Session Established.\r\n");
  printf("Cipher Name: %s\r\n", pSSLSession->SSLGetCipherName());
  printf("ECC Group Name: %s\r\n", pSSLSession->SSLGetECGroupName());
  printf("Start HTTP communication.......\r\n\r\n");
  
  // 发送HTTP请求
  string strRequest;
  strRequest = "GET / HTTP/1.1\r\n";
  strRequest += "Accept: */*\r\n";
  strRequest += "Connection: Close\r\n";
  //strRequest += "Accept-Encoding: gzip; br\r\n";  // 不支持压缩
  strRequest += "Host: ";
  strRequest += pszServer;
  strRequest += "\r\n\r\n";
  if(!pSSLSession->SSLSend((BYTE*)strRequest.c_str(), strRequest.length()))
  {
    printf("ERROR: SSLSend.\r\n");
    return -1;
  }
  /*
   接收HTTP响应数据
   1、iBuffSize将返回实际接收到的数据大小;
   2、如果接收的数据大于输入缓存arrBuff的尺寸,SSLRecv只会填满arrBuff缓存,
    后续数据将被丢弃。
   3、OnSSLHttpDataNotify,回调函数,业务层需要在回调函数中处理具体的业务数据,
    在本例中,使用开源的llhttp处理HTTP响应数据,如解析HTTP响应头,获取
    Content-Length字段大小或chunk,从而判断出后续要接收实际数据的尺寸。
    从而在llhttp的回调函数中通知上层用户。
    OnSSLHttpDataNotify返回值如下:
    3.1、SSL_DATA_RECV_STILL:业务层数据尚未读完,SSLRecv内部需要继续读取;
    3.2、SSL_DATA_RECV_FAILED:业务层出现错误,SSLRecv函数将返回FALSE;
    3.3、SSL_DATA_RECV_FINISHED:业务层数据处理完毕,SSLRecv函数将返回TRUE;
       本例中需要判断Content-Length来决定,业务层数据是否读取完毕。
    注:node.js中使用llhttp进行http数据解析,从而大幅提升解析效率
  */
  // 构造llhttp解析器,用于解析HTTP返回的响应数据
  llhttp_t llhttp_parser;
  llhttp_settings_t settings;
  llhttp_settings_init(&settings);
  settings.on_headers_complete = on_llhttp_headers_complete;    // http响应头已接收完毕通知
  settings.on_message_complete = on_llhttp_message_complete;    // http响应消息接收完毕
  settings.on_body = on_llhttp_body;                // http除响应头外的消息体数据
  llhttp_init(&llhttp_parser, HTTP_RESPONSE, &settings);
  HTTP_PARAMS http_params;
  llhttp_parser.data = (void*)&http_params;  // 用户自定义数据

  BYTE arrBuff[1024] = {0};
  int iBuffSize = 1024;
  // 将读取到的所有响应内容保存到文件中,SSL层上抛的数据
  const char* pszPathFile = "C:/TestSSLHttp.html";
  http_params.hFile = ::CreateFile(pszPathFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                    FILE_ATTRIBUTE_NORMAL, NULL);
  if(INVALID_HANDLE_VALUE == http_params.hFile)
  {
    printf("ERROR: CreateFile "%s".\r\n", pszPathFile);
    return -1;
  }
  // 将读取到的Body内容保存到文件中,llhttp处理后的真实body数据
  const char* pszPathFileBody = "C:/TestSSLHttp_body.html";
  http_params.hFileBody = ::CreateFile(pszPathFileBody, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                    FILE_ATTRIBUTE_NORMAL, NULL);
  if(INVALID_HANDLE_VALUE == http_params.hFileBody)
  {
    printf("ERROR: CreateFile "%s".\r\n", pszPathFileBody);
    return -1;
  }
  
  BOOL bRet = pSSLSession->SSLRecv(arrBuff, iBuffSize, OnSSLHttpDataNotify, (DWORD)&llhttp_parser, 0);
  if(!bRet)
  {
    printf("ERROR: SSLRecv.\r\n");
  }
  ::CloseHandle(http_params.hFile);
  ::CloseHandle(http_params.hFileBody);
  
  printf("\r\n====================HTTP Response Header====================\r\n");
  printf("%s", http_params.szResponseHeader);
  printf("\r\n====================HTTP Response Save To File====================\r\n");
  printf("Write all response data to file: "%s"\r\n", pszPathFile);
  printf("Write body data to file: "%s"\r\n", pszPathFileBody);
  printf("\r\n====================HTTP Response Finished====================\r\n");
  if(!http_params.bChunked)
  {
    printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nContent-Length = %I64u\r\nBody Length=%I64u\r\n", 
      http_params.i64TotalReaded, http_params.iResponseHeaderLen,
      http_params.i64TotalReaded-http_params.iResponseHeaderLen, 
      http_params.i64ContentLen, http_params.i64BodyLen);
  }
  else
  {
    printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nTransfer-Encoding = chunked\r\nBody Length = %I64u\r\n", 
      http_params.i64TotalReaded, http_params.iResponseHeaderLen,
      http_params.i64TotalReaded-http_params.iResponseHeaderLen, 
      http_params.i64BodyLen);
  }
  // !!释放ISSLSession接口
  sslWrap.ReleaseSSLSession(pSSLSession);
  printf("\r\nPress any key exit.....\r\n");
  getchar();
  return 0;
}
//=============================以下为公共函数=============================
// 字符串去除头尾的空格
void StrTrim(char* pszSrc) 
{
  if(NULL == pszSrc)
    return;
  int i = 0, j = 0;
  // 找到第一个非' '字符
  while (pszSrc[j] == ' ') {
    ++j;
  }
  // 如果字符串全为空
  if (pszSrc[j] == 0) {
    pszSrc[0] = 0;
    return;
  }
  int iIdx = j;    // 记录第一个非空字符位置
  int iStop = 0;
  while (pszSrc[j] != 0)
  {
    if (pszSrc[j] == ' ' && iStop == 0) {
      iStop = j;      // 记录后面遇到的一个空字符
    } 
    else if (pszSrc[j] != ' ' && iStop != 0) {
      iStop = 0;
    }
    // 将当前非空字符拷贝到以0为开始的新位置
    pszSrc[i++] = pszSrc[j++];
  }
  if (iStop > 0) {
    pszSrc[iStop - iIdx] = 0;
  } 
  else if (j != i) {
    pszSrc[i] = 0;
  }
}
// 解析HTTP 响应头
BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams)
{
  if(NULL == pHttpParams)
    return FALSE;
  int iLen = strlen(pHttpParams->szResponseHeader);
  char* pszResponseHeader = new char[iLen+1];
  strcpy(pszResponseHeader, pHttpParams->szResponseHeader);
  // 逐行解析
  int iPos = 0;
  char* pszKeyValue = pszResponseHeader;
  char* pszFind = strstr(pszKeyValue, CRLF);
  while(pszFind)
  {
    iPos = pszFind-pszKeyValue;
    *(pszKeyValue+iPos) = 0;
    if(0 == strlen(pszKeyValue))
      break;
    // 查找":",并解析key:Value,存放于mapHeader中,便于后续使用
    char* pszColon = strstr(pszKeyValue, ":");
    if(pszColon)
    {
      int iPosColon = pszColon - pszKeyValue;
      *(pszKeyValue+iPosColon) = 0;
      char* pszKey = pszKeyValue;
      char* pszValue = pszKeyValue + iPosColon + 1; // SKip Colon
      // 去除头尾空格
      StrTrim(pszKey);
      StrTrim(pszValue);
      // 保存到map中
      string strKey = pszKey;
      string strValue = pszValue;
      map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey);
      if(iter == pHttpParams->mapHeader.end())
      {
        pHttpParams->mapHeader.insert(map<string, string>::value_type(strKey, strValue));
      }
      else
      {
        iter->second += ";";
        iter->second += strValue;
      }
    }
    // 查找下一行
    pszKeyValue = pszKeyValue + iPos + 2;  // Skip "\r\n"
    pszFind = strstr(pszKeyValue, CRLF);
  }
  delete pszResponseHeader;
  return TRUE;
}
// 根据关键字获取对应的值
BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue)
{
  // 下面方法回出现由于key关键字的大小写不一,导致无法检索到
  //map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey);
  //if(iter == pHttpParams->mapHeader.end())
  //{
  //return FALSE;
  //}
  //strValue = iter->second;
  //return TRUE;
  map<string, string>::iterator  iter;
  for(iter = pHttpParams->mapHeader.begin(); iter != pHttpParams->mapHeader.end(); ++iter)
  {
    if(0 == _stricmp(iter->first.c_str(), strKey.c_str()))
    {
      strValue = iter->second;
      return TRUE;
    }
  }
  return FALSE;
}
 
DSC0001.png


  
关注下面的标签,发现更多相似文章