在探索http原理,使用C++基于socket自实现http中使用C++基于socket实现了http,本篇我将在之前实现http的基础上,使用openssl实现一个https。
https是指http和tcp之间多了一层ssl(Secure Sockets Layer 安全套接层)。http协议传输的是明文,就是发送端把数据写入socket,接收端从socket读取数据,数据在传输过程中都保持明文,非常容易被窃听,所以不安全。ssl就是用来实现加密传输的,大概流程就是发送端不再直接向socket写入数据,而是把数据写入ssl层,ssl将数据加密后再使用socket发出。接收端也不再直接从socket读取数据,而是ssl层从socket接收数据,此数据是加密的,ssl需要对数据解密,接收端应用程序从ssl读取解密后的数据。由此,数据在传输途中处于加密状态,从而是安全的。
ssl的加密过程比较复杂。涉及大量的密码学知识。这里大概介绍一下。
ssl的加密过程使用到了对称加密和非对称加密。
对称加密是指对信息的加密和解密都使用相同的密钥。也就是说,一把钥匙开一把琐。对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。但是对称加密的缺点也很明显,那就是密钥也需要在网上传输,一旦密钥被破解或截获,后果就可想而知了。
非对称加密算法需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。用得最多的非对称加密算法是RSA算法。用私钥对信息进行加密只有其对应的私钥才能解密,用私钥加密只有对应的公钥才能解密。这就能弥补对称加密的密钥不安全传输问题了,我们只需要把公钥发送出去,别人拿到了公钥也没办法解密信息。因为公钥加密的信息只有私钥才能解密,而私钥是有我们持有,只要保证私钥安全就可以了。非对称加密算法的缺点同样严重,那就是非对称加密算法的加解密速度比对称加密要慢上千倍,啊,这真是一个让人受不了的缺点。
结合对称加密和非对称加密的优缺点,于是人才们想出一个办法,对称加密的问题是密钥不安全,那我们就把这个密钥用非对称加密来加密一下再传输。这样几乎完美解决了这两个加密算法的问题。
说几乎解决是因为实际上还面临一个问题,那就是公钥的交换问题。如果通信的一端收到了对方发来的公钥,如何证明这个公钥是真实的对方发的,而不是中间有人假冒的。确实存在这种可能,比如张三和李四在通信,但中间存在一个老王,他跟张三说自己是李四,跟李四说自己是张三。于是就有了证书的概念,证书就是用来安全交换公钥的。证书类似于身份证,是用来证明身份的。身份证的公信力来源于政府,在加密界也存在一些有公信力的机构,称之为CA,CA是被操作系统信任的,相当于他的权威性写进了基因。我们可以让CA给我们证明一下身份,于是我们可以把公钥、域名和一些基本信息提交到CA。CA也有自己的公钥和私钥,CA用自己的私钥对我们的信息做一个摘要(比如MD5或SHA256等)并加密,这个加密只有CA的公钥才能解。我们提交到CA的信息加上CA的加密信息在一起就组成了证书。CA同时也把自己的公钥和基本信息摘要并加密生成一个证书,称为根证书,根证书被添加到操作系统里。验证证书时就用根证书内的公钥解密一下证书里的信息。其实证书就是证明和某个公钥确实是某人的,而不是冒充的。
为了实现https,所以我们先需要生成自己的证书,并把根证书添加到系统内,并信任这个这个根证书。openssl可以做这件事。
下载openssl
编译
./Configure
make install
生成CA的私钥
openssl genrsa -out rootCA.key 2048
生成CA的根证书
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem
执行完这个命令会让我们输入一些证书的信息
liaokun@192 http % openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:CN
State or Province Name (full name) []:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) []:LK Technology Co. Ltd.
Organizational Unit Name (eg, section) []:LK Technology Co. Ltd.
Common Name (eg, fully qualified host name) []:LK Technology Root CA
Email Address []:
执行完后,得到rootCA.pem文件,这就是根证书,我们把它添加到系统信任。
接下来,生成https的server端私钥
openssl genrsa -out server.key 2048
生成服务端的csr文件,ca根据csr文件为服务端颁发证书。可以把上面生成证书要填的信息写到-subj选项中,如下
openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=LK/OU=LK/CN=test.com"
ca根据csr文件为服务端颁发证书
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.pem -days 825 -sha256 -extfile cert/v3.ext
#SSL证书最长期限825天
server.pem就是https使用的服务端证书。我把地址写成了test.com。测试的时候需要改写/etc/hosts将本机地址映射到test.com。
接下来可以写代码了,在之前实现http的基础上,加入openssl。https默认使用的端口是443,所以服务端改为监听443端口。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define port 443 //监听端口,可以在范围内自由设定
using namespace std;
void close_ssl(SSL *ssl,int client_fd)
{
if(ssl)
{
/* 关闭 SSL 连接 */
SSL_shutdown(ssl);
/* 释放 SSL */
SSL_free(ssl);
}
if(client_fd>0)
{
close(client_fd);
}
}
int main()
{
std::string message ="";
message+="HTTP/1.1 200 OK";
message+="\r\n";
message+="Content-Type:text/html\r\n";
message+="server:Tengine \r\n";
message+="name:LiaoKun \r\n";
message+="\r\n";
message+="<html><head>Hello,World!</head></html>\r\n";
message+="\r\n";
SSL_CTX *ctx;
/* SSL 库初始化 */
SSL_library_init();
/* 载入所有 SSL 算法 */
OpenSSL_add_all_algorithms();
/* 载入所有 SSL 错误消息 */
SSL_load_error_strings();
/* 以 SSL V2 和 V3 标准兼容方式产生一个 SSL_CTX ,即 SSL Content Text */
ctx = SSL_CTX_new(SSLv23_server_method());
/* 也可以用 SSLv2_server_method() 或 SSLv3_server_method() 单独表示 V2 或 V3标准 */
if (ctx == NULL) {
ERR_print_errors_fp(stdout);
exit(1);
}
// 双向验证
// SSL_VERIFY_PEER---要求对证书进行认证,没有证书也会放行
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT---要求客户端需要提供证书,但验证发现单独使用没有证书也会放行
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
// SSL_CTX_set_security_level(ctx,0);
/* 载入用户的数字证书, 此证书用来发送给客户端。 证书里包含有公钥 */
if ( SSL_CTX_use_certificate_file(ctx, "/Users/liaokun/CLionProjects/http/cert/server.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stdout);
exit(1);
}
/* 载入用户私钥 */
if (SSL_CTX_use_PrivateKey_file(ctx, "/Users/liaokun/CLionProjects/http/cert/server.key", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stdout);
exit(1);
}
/* 检查用户私钥是否正确 */
if (!SSL_CTX_check_private_key(ctx)) {
ERR_print_errors_fp(stdout);
exit(1);
}
//1.创建一个socket套接字
int local_fd = socket(AF_INET, SOCK_STREAM, 0);
if (local_fd == -1)
{
cout << "socket error!" << endl;
exit(-1);
}
cout << "socket ready!" << endl;
//2.sockaddr_in结构体:可以存储一套网络地址(包括IP与端口),此处存储本机IP地址与本地的一个端口
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(port); //绑定6666端口
local_addr.sin_addr.s_addr = INADDR_ANY ; //绑定本机IP地址
//3.bind(): 将一个网络地址与一个套接字绑定,此处将本地地址绑定到一个套接字上
int res = bind(local_fd, (struct sockaddr *)&local_addr, sizeof(local_addr));
if (res == -1)
{
cout << "bind error!" << endl;
exit(-1);
}
cout << "bind ready!" << endl;
//4.listen()函数:监听试图连接本机的客户端
//参数二:监听的进程数
listen(local_fd, 10);
cout << "等待来自客户端的连接...." << endl;
SSL *ssl;
while (true)//循环接收客户端的请求
{
//5.创建一个sockaddr_in结构体,用来存储客户机的地址
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
//6.accept()函数:阻塞运行,直到收到某一客户机的连接请求,并返回客户机的描述符
int client_fd = accept(local_fd, (struct sockaddr *)&client_addr, &len);
if (client_fd == -1)
{
cout << "accept错误\n"<< endl;
exit(-1);
}
/* 基于 ctx 产生一个新的 SSL */
ssl = SSL_new(ctx);
/* 将连接用户的 socket 加入到 SSL */
SSL_set_fd(ssl, client_fd);
/* 建立 SSL 连接 */
if (SSL_accept(ssl) == -1) {
perror("accept");
printf("SSL 连接失败!\n");
close_ssl(ssl,client_fd);
break;
}
//7.输出客户机的信息
char *ip = inet_ntoa(client_addr.sin_addr);
cout << "客户机: " << ip << " 连接到本服务器成功!" << endl;
//8.输出客户机请求的信息
char buff[1024] = {0};
int size = SSL_read(ssl, buff, sizeof(buff));
cout << "Request information:\n"
<< buff << endl;
cout << size << " bytes" << endl;
//9.使用第6步accept()返回socket描述符,即客户机的描述符,进行通信。
SSL_write(ssl, message.c_str(), message.length());//返回message
close_ssl(ssl,client_fd);
}
/* 释放 CTX */
SSL_CTX_free(ctx);
close(local_fd);
return 0;
}
在浏览器请求,结果。