C++模擬Http/Https POST登錄web站點

C++模擬Http/Https訪問web站點

一、概述

1.HttpHttps的區別與聯繫

OSI參考模型中HttpHttps均屬於應用層協議。HttpHypertext Transfer Protocol,超文本傳輸協議;而HttpsSecure Hypertext Transfer Protocol安全超文本傳輸協議,它是一個安全通信通道,基於HTTP開發,用於在客戶端與服務器之間交換信息,它使用安全套接字層SSL進行信息交換,簡單來說它就是HTTP的安全版。

Http默認使用80端口,Https使用443端口。

Http的數據在網絡上是明文傳輸,而Https則是通過加密後的傳輸,因此它相比http會更加安全,但是由於需要額外加解密操作,因爲Https的效率沒有那麼高。在登錄Https站點和Http站點時,可以明顯感覺到性能差異。

2.關於web訪問的安全性

當前許多的web站點登錄時都是採用普通的http進行傳輸,這種方式有着極大的安全隱患。當前web開發登錄系統常用的有以下四種方式:

1) 賬號和密碼完全沒有加密,明文傳送。這種方式的安全級別是最低的,它無疑是將自己的賬號和密碼直接暴露給別人,通過抓包工具(例:WireShark)可以很容易的截獲到賬號和密碼。

2) 密碼採用MD5或其它加密方式進行加密,聲稱不可破解。其實,完全沒有必要破解,只要截獲加密後的密碼串,就可以以你的身份訪問服務器,這樣也是可以通過認證授權的。這種方式在加密程度上有了一定程度的提高,但仍是不安全的。

3) 客戶端在登錄前去服務端拿一次密鑰,通過該密鑰進行加密,而服務器端的密鑰是隨機生成的,每次訪問均會用不同的密鑰。這種方式的安全性比較高。

4) 採用“安全性最高”的HTTPS方式傳輸,客戶端與服務端會經過認證,且中間的傳輸數據全部進行加密。之所以在安全性最高上加引號,是因爲它也不是絕對安全的,比如前段時間Openssl曝出安全漏洞,大名鼎鼎的“心臟出血”,黑客利用它的一個memcpybug,可以從溢出的內存中拿到64K的用戶數據,導致用戶信息泄露。但是這個安全性級別相對前面三個是最高的,當前服務端的證書一年收費大約3-5千,用這點錢換來相對安全,是很划算的事情了。

二、SOCKET發送HTTP請求

1.基本流程

無論是Http還是Https都是基於TCP進行傳輸的,因此使用SOCKET模擬HTTP訪問web站點的方式,很簡單,就是將頭部數據拼接成數據包,發送給服務端,然後接收返回再解析就可以了。

其基本流程和編寫普通SOCKET通信是一樣的。Windows下的流程爲:

a. WSAStartupWinsock服務進行初始化

b. 建立socket套接字

c. connect連接服務端

d. send發送數據

e. recv接收數據

下面,以某站點的登錄爲例,利用Fiddler抓到的POST的頭部信息如下:

                                            

這樣,我們就可以構建這樣的數據包發送出去,然後接收響應了,C++實現核心代碼請見下部分。

2.核心代碼

BOOL SocketClient::ConnectToServer(const CString strServerUrl, const int nPort)
{
	cstrServerUrl = strServerUrl;
	nServerPort = nPort;
	BOOL bRet = FALSE;

	do 
	{
		if (!InitializeContext())
		{
			break;
		}

		if(!Connect())
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);
	return bRet;
}

BOOL SocketClient::LoginToServer(const CString strUsername, const CString strPasswd)
{
	cstrUserName = strUsername;
	cstrPassWord = strPasswd;
	BOOL bRet = FALSE;

	do 
	{
		if (!SendPostData())
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL SocketClient::LogoutOfServer()
{
	return FALSE;
}

BOOL SocketClient::InitializeContext()
{
	BOOL bRet = FALSE;
	wsaData = new WSADATA;
	WORD wVersion = MAKEWORD(2, 2);

	do 
	{
		if(0 != WSAStartup(wVersion, wsaData))
		{
			break;
		}

		if(LOBYTE( wsaData->wVersion ) != 2 || HIBYTE( wsaData->wVersion ) != 2 )
		{
			WSACleanup();
			break;
		}

		LPHOSTENT lpHostTent;
		lpHostTent = gethostbyname(cstrServerUrl);
		if (NULL == lpHostTent)
		{
			break;
		}

		socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketClient == INVALID_SOCKET)
		{
			WSACleanup();
			break;
		}

		socketAddrClient = new SOCKADDR_IN;
		socketAddrClient->sin_family = AF_INET;
		socketAddrClient->sin_port = htons(nServerPort);
		socketAddrClient->sin_addr = *((LPIN_ADDR)*lpHostTent->h_addr_list);
		memset(socketAddrClient->sin_zero, 0, sizeof(socketAddrClient->sin_zero));

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL SocketClient::Connect()
{
	BOOL bRet = FALSE;

	do 
	{
		if (SOCKET_ERROR == connect(socketClient, (LPSOCKADDR)socketAddrClient, sizeof(SOCKADDR_IN)))
		{
			 int nErrorCode = WSAGetLastError();
			closesocket(socketClient);
			break;
		}

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL SocketClient::SendPostData()
{
	CString cstrSendData;
	CString cstrSendParam = "redirect=&username="+cstrUserName+"&password="+cstrPassWord+"&auto_login=checked&submit=%E7%99%BB%E5%BD%95";
	BOOL bRet = FALSE;

	CString cstrSendParamLen;
	cstrSendParamLen.Format("%d", cstrSendParam.GetLength());

	cstrSendData = "POST http://account.vsochina.com/user/login HTTP/1.1\r\n";
	cstrSendData += "Host: account.vsochina.com\r\n";
	cstrSendData += "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0\r\n";
	cstrSendData += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n";
	cstrSendData += "Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3\r\n";
	cstrSendData += "Accept-Encoding: gzip, deflate\r\n";
	cstrSendData += "DNT: 1\r\n";
	cstrSendData += "Referer: http://account.vsochina.com/user/login\r\n";
	cstrSendData += "Connection: keep-alive\r\n";
	cstrSendData += "Content-Type: application/x-www-form-urlencoded\r\n";
	cstrSendData += "Content-Length: " + cstrSendParamLen +"\r\n";
	cstrSendData += "\r\n";
	cstrSendData += cstrSendParam;

	CString cstrRecvData;
	do 
	{
		if (-1 == send(socketClient, cstrSendData.GetBuffer(), cstrSendData.GetLength(), 0))
		{
			break;
		}

		char recvData[1000] = {0};
		int nRecvLen;

		while((nRecvLen = recv(socketClient, recvData, sizeof(recvData), 0)) > 0)
		{
			cstrRecvData += recvData;
		}

		if (cstrRecvData.GetLength() == 0)
		{
			break;
		}

		ParseCookieFromRecvData(cstrRecvData);

		//!判斷返回的COOKIE信息中,UID是否存在
		if (cstrCookieUid.IsEmpty())
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);


	return bRet;
}

void SocketClient::ParseCookieFromRecvData(const CString cstrRecvData)
{
	list<CString> lstCookiesLine;        //!存放Set-Cookie的一行,例:Set-Cookie: vso_uname=houqd_1111;
	CString cstrFind = "Set-Cookie:";    //!查找標記
	CString cstrSeperator = "\r\n";      //!以"\r\n"分割號來分割字符串

	int nPos = 0;
	int nStart = cstrRecvData.Find(cstrSeperator);

	while(nStart != -1)
	{
		CString cstrSessionLine = cstrRecvData.Mid(nPos, nStart - nPos + 1);

		if (cstrSessionLine.Find(cstrFind) != -1)
		{
			CString cstrRealRecord = cstrSessionLine.Right(cstrSessionLine.GetLength() - cstrFind.GetLength() - 3);
			list<CString>::iterator it = find(lstCookiesLine.begin(), lstCookiesLine.end(), cstrRealRecord);
			if (it == lstCookiesLine.end())
			{
				lstCookiesLine.push_back(cstrRealRecord);
			}
		}

		nPos = nStart;
		nStart = cstrRecvData.Find(cstrSeperator, nPos + 2);
	}

	//!根據每行獲取的cookie值,解析爲key-value的形式
	vector<CString> vecCookieSet;
	for (list<CString>::iterator it = lstCookiesLine.begin(); it != lstCookiesLine.end(); it++)
	{
		CString cstrCookies = *it;
		CString cstrSeperator = ";";
		StaticUtility::StringSplit(cstrCookies, cstrSeperator, vecCookieSet);
	}

	vector<CString> vecTemp;
	for (vector<CString>::iterator it = vecCookieSet.begin(); it != vecCookieSet.end(); it++)
	{
		vecTemp.clear();
		CString cstrOneCookies = *it;
		CString cstrSeperator = "=";

		StaticUtility::StringSplit(cstrOneCookies, cstrSeperator, vecTemp);
		CString cstrKey = vecTemp[0];
		CString cstrVal = vecTemp[1];

		if(cstrKey.Compare("vso_uid") == 0)
		{
			cstrCookieUid = cstrVal;
			break;
		}
	}
}

通過接收來的頭部信息中,將cookie信息解析出來,就可以判斷是否登錄成功了。然後,如果有或許的操作,在請求中掛上這些cookie信息,就可以獲取想要的數據,完成想要的操作了。

三、OpenSSL發送HTTPS請求

1.基本流程

HTTPS=HTTP + SSL,因此利用OpenSSL發送請求給HTTPS站點和第二章的SOCKET發送HTTP是非常相似的,只不過要在原生的套接字上套上SSL層,基本流程如下:

a. WSAStartupWinsock服務進行初始化

b. 建立socket套接字

c. connect連接服務端

d. 建立SSL上下文

e. 建立SSL

f. 將SSL與前面建立的socket套接字綁定

g. SSL_write()發送數據

h. SSL_read()接收數據

下面以小米官網站點的登錄爲例,來展示利用OpenSSL如何訪問HTTPS站點,模擬登陸,核心代碼,見下一章節。

2.核心代碼

#pragma comment( lib, "libeay32.lib" )
#pragma comment( lib, "ssleay32.lib" )
HttpsClient::HttpsClient(void):
			 wsaData(NULL),
		     socketAddrClient(NULL),
			 ssl(NULL),
			 sslCtx(NULL),
			 sslMethod(NULL),
			 serverCertification(NULL)
{
	SSL_load_error_strings();
	SSLeay_add_ssl_algorithms();
}


HttpsClient::~HttpsClient(void)
{
	//!清理打開的句柄
	if (NULL != ssl)
	{
		SSL_shutdown(ssl);
		closesocket(socketClient);
		SSL_free(ssl);
		ssl = NULL;
	}

	if (NULL != sslCtx)
	{
		SSL_CTX_free(sslCtx);
	}

	WSACleanup();
}

BOOL HttpsClient::ConnectToServer(const CString strServerUrl, const int nPort)
{
	cstrServerUrl = strServerUrl;
	nServerPort = nPort;
	BOOL bRet = FALSE;

	do 
	{
		if (!InitializeSocketContext())
		{
			break;
		}

		if (!SocketConnect())
		{
			break;
		}

		if (!InitializeSslContext())
		{
			break;
		}

		if (!SslConnect())
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);
	return bRet;
}

BOOL HttpsClient::LoginToServer(const CString strUsername, const CString strPasswd)
{
	cstrUserName = strUsername;
	cstrPassWord = strPasswd;
	BOOL bRet = FALSE;

	do 
	{
		if (!SendLoginPostData())
		{
			break;
		}

		CString cstrRecvData;
		RecvLoginPostData(cstrRecvData);
		if (cstrRecvData.GetLength() == 0)
		{
			break;
		}

		ParseCookieFromRecvData(cstrRecvData);

		if (cstrCookieUid.IsEmpty() || cstrCookieUid.Compare("EXPIRED") == 0)
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);
	return bRet;
}

BOOL HttpsClient::LogoutOfServer()
{
	return FALSE;
}

BOOL HttpsClient::InitializeSocketContext()
{
	//!初始化winSocket環境
	BOOL bRet = FALSE;
	wsaData = new WSADATA;
	WORD wVersion = MAKEWORD(2, 2);

	do 
	{
		if(0 != WSAStartup(wVersion, wsaData))
		{
			break;
		}

		if(LOBYTE( wsaData->wVersion ) != 2 || HIBYTE( wsaData->wVersion ) != 2 )
		{
			WSACleanup();
			break;
		}

		LPHOSTENT lpHostTent;
		lpHostTent = gethostbyname(cstrServerUrl);
		if (NULL == lpHostTent)
		{
			break;
		}

		socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketClient == INVALID_SOCKET)
		{
			WSACleanup();
			break;
		}

		socketAddrClient = new SOCKADDR_IN;
		socketAddrClient->sin_family = AF_INET;
		socketAddrClient->sin_port = htons(nServerPort);
		socketAddrClient->sin_addr = *((LPIN_ADDR)*lpHostTent->h_addr_list);
		memset(socketAddrClient->sin_zero, 0, sizeof(socketAddrClient->sin_zero));

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL HttpsClient::SocketConnect()
{
	//!原生socket連接
	BOOL bRet = FALSE;

	do 
	{
		if (SOCKET_ERROR == connect(socketClient, (LPSOCKADDR)socketAddrClient, sizeof(SOCKADDR_IN)))
		{
			int nErrorCode = WSAGetLastError();
			closesocket(socketClient);
			break;
		}

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL HttpsClient::InitializeSslContext()
{
	//!SSL通信初始化
	BOOL bRet = FALSE;

	do 
	{
		sslMethod = SSLv23_client_method();
		if(NULL == sslMethod)
		{
			break;
		}

		sslCtx = SSL_CTX_new(sslMethod);
		if (NULL == sslCtx)
		{
			break;
		}

		ssl = SSL_new(sslCtx);
		if (NULL == ssl)
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL HttpsClient::SslConnect()
{
	//!SSL綁定原生socket,並連接服務器
	BOOL bRet = FALSE;

	do 
	{
		SSL_set_fd(ssl, socketClient);

		int nRet = SSL_connect(ssl);
		if (-1 == nRet)
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL HttpsClient::SslGetCipherAndCertification()
{
	BOOL bRet = FALSE;

	do 
	{
		cstrSslCipher = SSL_get_cipher(ssl);
		serverCertification = SSL_get_certificate(ssl);

		if (NULL == serverCertification)
		{
			break;
		}

		cstrSslSubject = X509_NAME_oneline(X509_get_subject_name(serverCertification), 0, 0);
		cstrSslIssuer = X509_NAME_oneline(X509_get_issuer_name(serverCertification), 0, 0);

		X509_free(serverCertification);

		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

BOOL HttpsClient::SendLoginPostData()
{
	CString cstrSendData;
	//CString cstrSendParam = "redirect=&username="+cstrUserName+"&password="+cstrPassWord+"&auto_login=checked&submit=%E7%99%BB%E5%BD%95";
	CString cstrSendParam = "user="+cstrUserName+"&_json=true&pwd="+cstrPassWord+"&callback=http%3A%2F%2Forder.mi.com%2Flogin%2Fcallback%3Ffollowup%3Dhttp%253A%252F%252Fwww.mi.com%252F%26sign%3DNWU4MzRmNjBhZmU4MDRmNmZkYzVjMTZhMGVlMGFmMTllMGY0ZTNhZQ%2C%2C&sid=mi_eshop&qs=%253Fcallback%253Dhttp%25253A%25252F%25252Forder.mi.com%25252Flogin%25252Fcallback%25253Ffollowup%25253Dhttp%2525253A%2525252F%2525252Fwww.mi.com%2525252F%252526sign%25253DNWU4MzRmNjBhZmU4MDRmNmZkYzVjMTZhMGVlMGFmMTllMGY0ZTNhZQ%25252C%25252C%2526sid%253Dmi_eshop&hidden=&_sign=%2Bw73Dr7cAfRlMfOR6fW%2BF0QG4jE%3D&serviceParam=%7B%22checkSafePhone%22%3Afalse%7D&captCode=";
	BOOL bRet = FALSE;

	CString cstrSendParamLen;
	cstrSendParamLen.Format("%d", cstrSendParam.GetLength());

	cstrSendData = "POST https://account.xiaomi.com/pass/serviceLoginAuth2 HTTP/1.1\r\n";
	cstrSendData += "Host: account.xiaomi.com\r\n";
	cstrSendData += "User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0\r\n";
	cstrSendData += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n";
	cstrSendData += "Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3\r\n";
	cstrSendData += "Accept-Encoding: gzip, deflate\r\n";
	cstrSendData += "DNT: 1\r\n";
	cstrSendData += "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n";
	cstrSendData += "Referer: https://account.xiaomi.com/pass/serviceLogin?callback=http%3A%2F%2Forder.mi.com%2Flogin%2Fcallback%3Ffollowup%3Dhttp%253A%252F%252Fwww.mi.com%252Findex.html%26sign%3DNDRhYjQwYmNlZTg2ZGJhZjI0MTJjY2ZiMTNiZWExODMwYjkwNzg2ZQ%2C%2C&sid=mi_eshop\r\n";
	cstrSendData += "Content-Length: " + cstrSendParamLen +"\r\n";
	cstrSendData += "Connection: keep-alive\r\n";	
	cstrSendData += "\r\n";
	cstrSendData += cstrSendParam;

	CString cstrRecvData;
	do 
	{
		int nRet = SSL_write(ssl, cstrSendData, cstrSendData.GetLength());

		if(-1 == nRet)
		{
			break;
		}
        
		bRet = TRUE;
	} while (FALSE);

	return bRet;
}

void HttpsClient::RecvLoginPostData(CString &cstrRecvData)
{
	BOOL bRet = FALSE;

	do 
	{
		TIMEVAL tval;
		tval.tv_sec = 20;
		tval.tv_usec = 0;

		while(TRUE)
		{
			FD_SET fds;
			FD_ZERO(&fds);
			FD_SET(socketClient, &fds);

			char recvData[1000] = {0};
			int nRecvLen;

			//int nSelect = select(FD_SETSIZE, &fds, NULL, NULL, &tval);
			//if (1 != nSelect)
			//{
			//	break;
			//}

			int nErr = SSL_read(ssl, recvData, sizeof(recvData));
			if (nErr <= 0)
			{
				break;
			}

			cstrRecvData += recvData;
		}

		if (cstrRecvData.GetLength() == 0)
		{
			break;
		}

		bRet = TRUE;
	} while (FALSE);
}

void HttpsClient::ParseCookieFromRecvData(const CString cstrRecvData)
{
	list<CString> lstCookiesLine;        //!存放Set-Cookie的一行,例:Set-Cookie: vso_uname=houqd_1111;
	CString cstrFind = "Set-Cookie:";    //!查找標記
	CString cstrSeperator = "\r\n";      //!以"\r\n"分割號來分割字符串

	int nPos = 0;
	int nStart = cstrRecvData.Find(cstrSeperator);

	while(nStart != -1)
	{
		CString cstrSessionLine = cstrRecvData.Mid(nPos, nStart - nPos + 1);

		if (cstrSessionLine.Find(cstrFind) != -1)
		{
			CString cstrRealRecord = cstrSessionLine.Right(cstrSessionLine.GetLength() - cstrFind.GetLength() - 3);
			list<CString>::iterator it = find(lstCookiesLine.begin(), lstCookiesLine.end(), cstrRealRecord);
			if (it == lstCookiesLine.end())
			{
				lstCookiesLine.push_back(cstrRealRecord);
			}
		}

		nPos = nStart;
		nStart = cstrRecvData.Find(cstrSeperator, nPos + 2);
	}

	//!根據每行獲取的cookie值,解析爲key-value的形式
	vector<CString> vecCookieSet;
	for (list<CString>::iterator it = lstCookiesLine.begin(); it != lstCookiesLine.end(); it++)
	{
		CString cstrCookies = *it;
		CString cstrSeperator = ";";
		StaticUtility::StringSplit(cstrCookies, cstrSeperator, vecCookieSet);
	}

	vector<CString> vecTemp;
	for (vector<CString>::iterator it = vecCookieSet.begin(); it != vecCookieSet.end(); it++)
	{
		vecTemp.clear();
		CString cstrOneCookies = *it;
		CString cstrSeperator = "=";

		StaticUtility::StringSplit(cstrOneCookies, cstrSeperator, vecTemp);
		CString cstrKey;
		CString cstrVal;

		if (vecTemp.size() == 2)
		{
			cstrKey = vecTemp[0];
			cstrVal = vecTemp[1];
		}

		if(cstrKey.Compare("userId") == 0)
		{
			cstrCookieUid = cstrVal;
			break;
		}
	}
}

同理,判斷登錄也是在返回的信息中拿cookie信息,再進行下一步操作。

四、參考資料及代碼下載鏈接

1) 使用OpenSSL API進行安全編程:http://www.ibm.com/developerworks/cn/linux/l-openssl.html

2) SSL建立過程分析:http://blog.chinaunix.net/uid-127037-id-2919489.html

3) Openssl使用:http://www.cppblog.com/woomsg/archive/2008/11/03/64508.html

4) 知乎OpenSSLhttp://www.zhihu.com/topic/19584279

5) Retrieving a file via HTTPhttp://www.codeproject.com/Articles/1876/Retrieving-a-file-via-HTTP

6) Step in a Typical HTTP Client Applicationhttps://msdn.microsoft.com/en-us/library/8yh4zs9e%28v=VS.80%29.aspx

7) Make a POST HTTP request over a sockethttp://paul.grozav.info/2013/05/16/c-make-a-post-http-request-over-a-socket/

8) C++ STL中哈希表hash_map介紹:blog.csdn.net/ddkxddkx/article/details/6555754

9) 我的代碼實現的下載鏈接爲:http://download.csdn.net/detail/houqingdong2012/8540317

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