本文是在学习https的时候,参照网上诸多资料,在实际操作过程中还是遇到不少问题,力求详细的阐述在linux环境下,c++使用openssl对http报文加密成为https的过程,并使用tcpdump抓取tcp报文展示http和https在传输过程中的差别。
本文适合对c++ openssl一点了解都没有,服务器端编程,只需要把ssl添加上tcp连接上。
参照文章:
C++通过openssl搭建https服务器
OpenSSL生成CA自签名根证书和颁发证书
tcpdump抓取TCP/IP数据包分析
openssl基本原理 + 生成证书 + 使用实例
总述:https相当于在http连接上添加上一层ssl验证。
如图所示,与没有ssl验证相比,在服务器端,listen之前设置ssl证书,在accept之后使用ssl_accept进行ssl的连接,相当于套了一层壳。
http.cpp
/*
接受一个tcp请求,简简单单发送发送一个http响应报文
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main(int argc, char* argv[]){
if(argc < 3){
printf("need filename ip-address port\n");
return 1;
}
//ip地址
char *ip = argv[1];
//端口
int port = atoi(argv[2]);
//创建socket,ipv4.tcp
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
printf("Create socket error %d", errno);
return 1;
}
//命名socket
//创建ipv4地址
struct sockaddr_in m_addr;
bzero(&m_addr, sizeof(m_addr));
m_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &m_addr.sin_addr);
m_addr.sin_port = htons(port);
//绑定
int ret = bind(listenfd, (struct sockaddr*)&m_addr, sizeof(m_addr));
if(ret == -1){
printf("Socket bind error %d", ret);
return 1;
}
//监听
ret = listen(listenfd, 100);
if(ret == -1){
printf("Listen error %d", ret);
return 1;
}
while(1){
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
int new_con = accept(listenfd, (sockaddr *)&addr, &addrlen);
if(new_con == -1){
printf("accept error, errno = %d",errno);
continue;
} else {
printf("accept %d success\n", new_con);
}
string html_file = "welcome.html";
int fd = open(html_file.c_str(), O_RDONLY);
struct stat file_stat;
stat(html_file.c_str(), &file_stat);
void *html_ = mmap(nullptr, file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
string buf_w = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n"
"Connection: close\r\n"
"Date: Fri, 23 Nov 2018 02:01:05 GMT\r\n"
"Content-Length: " + to_string(file_stat.st_size) + "\r\n"
"\r\n";
buf_w += (char *)html_;
printf("send %d bytes\n", send(new_con, (void*)buf_w.c_str(), buf_w.size(), 0));
munmap(html_, file_stat.st_size);
sleep(2);
close(new_con);
}
return 0;
}
其中welcome.html
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebServertitle>
head>
<body>
<br/>
<br/>
<div align="center"><font size="5"> <strong>hello httpstrong>font>div>
<br/>
<br/>
<form action="5" method="post">
<div align="center"><button type="submit">点赞button>div>
form>
<br/>
<form action="6" method="post">
<div align="center"><button type="submit" >收藏button>div>
form>
<br/>
<form action="7" method="post">
<div align="center"><button type="submit">关注button>div>
form>
div>
body>
html>
注意,html文件要和http.cpp文件在同一文件夹下才能被准确打开。
文件结构:
<pre>.
├── http
├── http.cpp
└── welcome.html
pre>
编译完成后,在http所在的位置命令行键入$ ./http 127.0.0.1 1234
开始等待连接。
同时另开一终端,输入$sudo tcpdump -A -i lo port 1234
开始监听本地回环lo的1234端口的信息。
打开postman发送get报文
或者打开浏览器,输入127.0.0.1:1234/
此时tcpdump抓取到的信息,可以看到是明文传输的,这也是问什么需要https的原因。
[zsz@localhost https]$ sudo tcpdump -A -i lo port 1234
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
22:11:03.930244 IP localhost.50744 > localhost.search-agent: Flags [S], seq 741845069, win 43690, options [mss 65495,sackOK,TS val 1659589 ecr 0,nop,wscale 7], length 0
E..<..@[email protected]..,7.M.........0.........
..R.........
22:11:03.930333 IP localhost.search-agent > localhost.50744: Flags [S.], seq 2115968623, ack 741845070, win 43690, options [mss 65495,sackOK,TS val 1659590 ecr 1659589,nop,wscale 7], length 0
E..<..@.@.<............8~..o,7.N.....0.........
..R...R.....
22:11:03.930368 IP localhost.50744 > localhost.search-agent: Flags [.], ack 1, win 342, options [nop,nop,TS val 1659590 ecr 1659590], length 0
E..4..@[email protected]..,7.N~..p...V.(.....
..R...R.
22:11:03.930742 IP localhost.search-agent > localhost.50744: Flags [P.], seq 1:841, ack 1, win 342, options [nop,nop,TS val 1659590 ecr 1659590], length 840
E..|L.@[email protected]~..p,7.N...V.q.....
..R...R.HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Connection: close
Date: Fri, 23 Nov 2018 02:01:05 GMT
Content-Length: 704
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebServertitle>
head>
<body>
<br/>
<br/>
<div align="center"><font size="5"> <strong>hello httpstrong>font>div>
<br/>
<br/>
<form action="5" method="post">
<div align="center"><button type="submit">......button>div>
form>
<br/>
<form action="6" method="post">
<div align="center"><button type="submit" >......button>div>
form>
<br/>
<form action="7" method="post">
<div align="center"><button type="submit">......button>div>
form>
div>
body>
html>
22:11:03.930768 IP localhost.50744 > localhost.search-agent: Flags [.], ack 841, win 355, options [nop,nop,TS val 1659590 ecr 1659590], length 0
E..4..@[email protected]..,7.N~.!....c.(.....
..R...R.
22:11:03.931909 IP localhost.50744 > localhost.search-agent: Flags [P.], seq 1:202, ack 841, win 355, options [nop,nop,TS val 1659592 ecr 1659590], length 201
E.....@[email protected]..,7.N~.!....c.......
..R...R.GET / HTTP/1.1
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Postman-Token: 9d205642-3df0-49c7-87a5-74e73f2d731d
Host: 127.0.0.1:1234
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
https需要证书,可以使用openssl进行自签发证书。
详细可以自行搜索
下边三行分别是生成私钥,证书申请文件,生成ca证书.
注意生成私钥是要键入密码的,证书申请和生成要填很多信息,看说明填入。
openssl genrsa -des3 -out privkey.pem 2048
openssl req -new -key privkey.pem -out cert.csr
openssl x509 -req -in cert.csr -out cert.pem -signkey privkey.pem -days 3650
最后会生成privkey.pem和cert.pem,分别用来ssl验证私钥和证书。
和http.cpp的区别就是一开始多了初始化,在accept之后用SSL_accept再次接受一次。
https.cpp
使用g++编译的时候要加上-lssl -lcrypto
两个库
/*
接受一个tcp请求,简简单单发送发送一个http响应报文
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "assert.h"
using namespace std;
SSL_CTX *ctx = NULL;
bool InitSSL(const char* cacert, const char* key, const char* passwd){
// 初始化
SSLeay_add_ssl_algorithms();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
ERR_load_BIO_strings();
// 我们使用SSL V3,V2
assert((ctx = SSL_CTX_new(SSLv23_method())) != NULL);
// 要求校验对方证书,这里建议使用SSL_VERIFY_FAIL_IF_NO_PEER_CERT,详见https://blog.csdn.net/u013919153/article/details/78616737
//对于服务器端来说如果使用的是SSL_VERIFY_PEER且服务器端没有考虑对方没交证书的情况,会出现只能访问一次,第二次访问就失败的情况。
SSL_CTX_set_verify(ctx, SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
// 加载CA的证书
assert(SSL_CTX_load_verify_locations(ctx, cacert, NULL));
// 加载自己的证书
assert(SSL_CTX_use_certificate_chain_file(ctx, cacert) > 0);
//assert(SSL_CTX_use_certificate_file(ctx, "cacert.pem", SSL_FILETYPE_PEM) > 0);
// 加载自己的私钥
SSL_CTX_set_default_passwd_cb_userdata(ctx, (void*)passwd);
assert(SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) > 0);
// 判定私钥是否正确
assert(SSL_CTX_check_private_key(ctx));
return true;
}
int main(int argc, char* argv[]){
string cacert = "cert.pem";
string key = "privkey.pem";
string passwd = "123456";
if(!InitSSL(cacert.c_str(), key.c_str(), passwd.c_str())){
printf("init ssl error\n");
return 0;
}
if(argc < 3){
printf("need filename ip-address port\n");
return 1;
}
//ip地址
char *ip = argv[1];
//端口
int port = atoi(argv[2]);
//创建socket,ipv4.tcp
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
printf("Create socket error %d", errno);
return 1;
}
//命名socket
//创建ipv4地址
struct sockaddr_in m_addr;
bzero(&m_addr, sizeof(m_addr));
m_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &m_addr.sin_addr);
m_addr.sin_port = htons(port);
//绑定
int ret = bind(listenfd, (struct sockaddr*)&m_addr, sizeof(m_addr));
if(ret == -1){
printf("Socket bind error %d", ret);
return 1;
}
//监听
ret = listen(listenfd, 100);
if(ret == -1){
printf("Listen error %d", ret);
return 1;
}
while(1){
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
int new_con = accept(listenfd, (sockaddr *)&addr, &addrlen);
if(new_con == -1){
printf("accept error, errno = %d",errno);
continue;
} else {
printf("accept %d success\n", new_con);
}
//ssl
SSL *ssl = SSL_new(ctx);
if(ssl == NULL)
{
printf("ssl new wrong\n");
return 0;
}
SSL_set_accept_state(ssl);
//关联sockfd和ssl
SSL_set_fd(ssl, new_con);
int ret = SSL_accept(ssl);
if(ret != 1){
printf("%s\n", SSL_state_string_long(ssl));
printf("ret = %d, ssl get error %d\n", ret, SSL_get_error(ssl, ret));
}
//
string html_file = "welcome.html";
int fd = open(html_file.c_str(), O_RDONLY);
struct stat file_stat;
stat(html_file.c_str(), &file_stat);
void *html_ = mmap(nullptr, file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
string buf_w = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n"
"Connection: close\r\n"
"Date: Fri, 23 Nov 2018 02:01:05 GMT\r\n"
"Content-Length: " + to_string(file_stat.st_size) + "\r\n"
"\r\n";
buf_w += (char *)html_;
//把send换成SSL_write
//printf("send %d bytes\n", send(new_con, (void*)buf_w.c_str(), buf_w.size(), 0));
printf("send %d bytes\n", SSL_write(ssl, (void*)buf_w.c_str(), buf_w.size()));
munmap(html_, file_stat.st_size);
//关闭
SSL_shutdown(ssl);
SSL_free(ssl);
close(new_con);
}
SSL_CTX_free(ctx);
return 0;
}
如果使用postman,要把证书验证关掉,因为这个证书是我们自己发布的,不是官方,
在ip前边加上https前缀
如果使用浏览器,会提示不安全连接,直接同意就行了,要注意https前缀
和前边一样使用tcpdump抓包,可以看到都是乱码
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
23:00:32.693162 IP localhost.51284 > localhost.search-agent: Flags [S], seq 956600350, win 43690, options [mss 65495,sackOK,TS val 4628353 ecr 0,nop,wscale 7], length 0
E..<..@[email protected].........
.F..........
23:00:32.693283 IP localhost.search-agent > localhost.51284: Flags [S.], seq 2464157102, ack 956600351, win 43690, options [mss 65495,sackOK,TS val 4628353 ecr 4628353,nop,wscale 7], length 0
E..<..@.@.<............T....9........0.........
.F...F......
23:00:32.693385 IP localhost.51284 > localhost.search-agent: Flags [.], ack 1, win 342, options [nop,nop,TS val 4628353 ecr 4628353], length 0
E..4..@[email protected]".........T..9..........V.(.....
.F...F..
23:00:32.700124 IP localhost.51284 > localhost.search-agent: Flags [P.], seq 1:518, ack 1, win 342, options [nop,nop,TS val 4628360 ecr 4628353], length 517
E..9..@.@.+..........T..9..........V.......
.F...F.............G5.E...YS.h...T........$.Dd.r.;. .......Sid.,]
L..#EH.X...... .&'.$.......,.
.+... .0.../.......5.../.
..............
.......................#........#.lJ'.y..[....... .N..!../.........!..U......\
.z..y.L[...F.0......B."[email protected]%.....I}/.f..9...c..{.'j...... .
.j..^..s...+..;.p.......h2.http/1.1..........3.k.i... ...MtF>rzx.C.zU..+.8.fv..>mgb.zA...A.D.I....l6......)!E...tX[...D;i.s#.q.s..W...3..y.....0.4....IN$>Z.+....................................-........@................
23:00:32.700227 IP localhost.search-agent > localhost.51284: Flags [.], ack 518, win 350, options [nop,nop,TS val 4628360 ecr 4628360], length 0
E..4..@[email protected]..$...^.(.....
.F...F..
23:00:32.700410 IP localhost.search-agent > localhost.51284: Flags [F.], seq 1, ack 518, win 350, options [nop,nop,TS val 4628360 ecr 4628360], length 0
E..4./@[email protected]..$...^.(.....
.F...F..
23:00:32.700799 IP localhost.51284 > localhost.search-agent: Flags [F.], seq 518, ack 2, win 342, options [nop,nop,TS val 4628360 ecr 4628360], length 0
E..4..@[email protected] .........T..9..$.......V.(.....
.F...F..
23:00:32.700815 IP localhost.search-agent > localhost.51284: Flags [.], ack 519, win 350, options [nop,nop,TS val 4628360 ecr 4628360], length 0
E..4.0@[email protected]..%...^.(.....
.F...F..
在学习如何使用https的时候,一开始概念很清楚,很多文章也直接明了的说了是在http和tcp中间加上一层ssl验证,但是对于证书如何获取,以及在代码中如何体现却很难找到一篇完整论述的文章,大部分文章都是基于读者有一定基础来写,加上c++实现的https服务器基本是体量很大,很难在庞大的代码中找到自己想要的,也就是关于ssl如何建立的部分。
因此本文的主要目的不是说让读者建立一个完整的https的概念,或者掌握大部分openssl的api,而是说给出一个可以跑的通的代码,一个非常简洁的代码,一套跟着做就能实现的使用openssl+https的完整流程。
这篇文章也是留个自己的一个教程,之后建立https服务器的时候,也可以根据这篇文章结合其他文章迅速回忆起相关知识,直接建立服务。