文章目录
前面学习了什么是HTTPS协议,了解了HTTPS的工作原理以及具体的工作流程,了解了HTTP协议和HTTPS协议之间的区别。当然,纸上学来终觉浅,懂了原理还不行,还是得实际操作一遍才能真正的理解其工作流程。
下面通过之前所学的Telnet协议,HTTPS协议知识,结合起来进行实操练习,给自己一个目标,就是说实现一个安全性的Telnet服务 —— TelnetS服务
01 | TelnetS
HTTPS = HTTP + SSL/TLS
在网络编程中,主要体现到增加了证书校验,传输加密的过程
02 | OpenSSL
加密算法有很多,这里主要通过
OPENSSL
提供的API以示例代码进行学习
OPENSSL
是开源的安全的套接字层的数据传输加密库,主要提供了多种加密算法、密钥/证书管理、SSL协议等功能,整体开源包分为三个部分:
libssl, libcrypto, openssl
- libssl:SSL协议库
- libcrypto:加密算法库
- openssl:总体app命令工具(函数)库
通过官方的服务端例子源码进行学习,其中关键函数意义如下:
wiki.openssl.org
- 初始化
OPENSSL
库以下函数在OPENSSL的V1.1.0版本开始被OPENSSL_init_ssl()弃用``````#include<openssl/ssl.h>intSSL_library_init(void);voidSSL_load_error_strings(void);intOpenSSL_add_ssl_algorithms(void);
1. **SSL_library_init()**1. 描述:初始化SSL算法库函数2. 笔记:在进行任何其他操作之前,必须先调用该函数,且不可重复调用3. 返回值:始终返回 “1"2. **SSL_load_error_strings()**1. 描述:载入所有SSL 错误消息, 为所有 libcrypto 函数注册错误字符串, 注册libssl错误字符串2. 返回值:无3. **OpenSSL_add_ssl_algorithms()**1. 描述:载入所有SSL 算法, SSL_library_init()的同义函数,作为宏实现2. 返回值:始终为“1” - 加载 & 校验证书
#include<openssl/ssl.h>/* 描述:* * 笔记:在进行任何其他操作之前,必须先调用该函数,且不可重复调用* */SSL_CTX *SSL_CTX_new(const SSL_METHOD *method);intSSL_use_certificate_file(SSL *ssl,constchar*file,int type);intSSL_CTX_use_PrivateKey_file(SSL_CTX *ctx,constchar*file,int type);intSSL_CTX_check_private_key(const SSL_CTX *ctx);
1. SSL_CTX_new()1. 描述:创建一个SSL_CTX *
实例,用来保存证书的私钥,其中包含与 SSL/TLS 或 DTLS 会话建立相关的各种配置和数据;这些内容稍后由表示活动会话的 SSL 对象继承。2. method:可通过TLS_client_method()、TLS_server_method() 和TLS_method()
三个最新的库函数创建,旧版的SSLv23_method(), SSLv23_server_method(), SSLv23_client_method()
等库函数也依旧可用3. 返回值:1. 零:创建新的SSL_CTX对象失败。检查错误堆栈以找出原因;2. 指向SSL_CTX对象的指针:返回值指向已分配的SSL_CTX对象2. SSL_CTX_use_certificate_file()1. 描述:将存储在文件中的第一个证书加载到 ctx 中。证书的格式设置类型必须从已知类型SSL_FILETYPE_PEM、SSL_FILETYPE_ASN1中指定。SSL_use_certificate_file() 将证书从文件加载到 SSL 中2. 返回值:成功后,函数返回 1。否则,请检查错误堆栈以找出原因3. SSL_CTX_use_PrivateKey_file()1. 描述:将在文件中找到的第一个私钥添加到 ctx。私钥的格式设置类型必须从已知类型SSL_FILETYPE_PEM、SSL_FILETYPE_ASN1中指定2. 返回值:成功后,函数返回 1。否则,请检查错误堆栈以找出原因4. **SSL_CTX_check_private_key()**1. 描述:检查私钥与加载到 ctx 中的相应证书的一致性。如果安装了多个密钥/证书对 (RSA/DSA),则将检查最后安装的项目。例如,如果最后一项是 RSA 证书或密钥,则将检查 RSA 密钥/证书对2. 返回值:成功后,函数返回 1。否则,请检查错误堆栈以找出原因 - 创建与
socket
绑定的SSL
实例#include<openssl/ssl.h>SSL *SSL_new(SSL_CTX *ctx);intSSL_set_fd(SSL *ssl,int fd);
1. SSL_new()1. 描述:创建一个新的 SSL 结构,该结构是保存 TLS/SSL 连接数据所必需的。新结构继承了底层上下文 ctx 的设置:连接方法、选项、验证设置、超时设置。SSL 结构被计算为引用。首次创建 SSL 结构会增加引用计数。释放它(使用 SSL_free)会递减它。当引用计数降至零时,将释放分配给 SSL 结构的任何内存或资源2. 返回值:1. 0:新的 SSL 结构失败。检查错误堆栈以找出原因;2. 指向 SSL 结构的指针:返回值指向分配的 SSL 结构2. SSL_set_fd()1. 描述:将文件描述符 fd 设置为 SSL 的 TLS/SSL(加密)端的输入/输出工具。fd 通常是网络连接的套接字文件描述符。执行该操作时,会自动创建一个套接字 BIO 以在 SSL 和 fd 之间进行接口。BIO和SSL引擎继承了fd的行为。如果 fd 是非阻塞的,则 SSL 也将具有非阻塞行为。如果已经有一个BIO连接到ssl,将调用BIO_free()(对于读取和写入端,如果不同)2. 返回值1. 0:操作失败。检查错误堆栈以找出原因;2. 1:操作成功 SSL
握手,建立SSL
连接#include<openssl/ssl.h>intSSL_accept(SSL *ssl);
1. SSL_accept()1. 描述:等待 TLS/SSL 客户端启动 TLS/SSL 握手。必须已通过设置基础 BIOS 设置通信通道并将其分配给 ssl。2. 笔记:SSL_accept() 的行为取决于底层 BIO。如果底层 BIO 阻塞,则 SSL_accept() 仅在握手完成或发生错误后返回。如果底层 BIO 是非阻塞的,当底层 BIO 无法满足 SSL_accept() 继续握手的需求时,SSL_accept() 也会返回,通过返回值 -1 指示问题。在这种情况下,调用返回值为 SSL_accept() 的 SSL_get_error() 将产生 SSL_ERROR_WANT_READ 或SSL_ERROR_WANT_WRITE。然后,调用进程必须在采取适当的操作以满足 SSL_accept() 的需求后重复调用。操作取决于基础 BIO。使用非阻塞套接字时,无需执行任何操作,但 select() 可用于检查所需条件。使用缓冲 BIO 对(如 BIO 对)时,必须先将数据写入或检索出 BIO,然后才能继续3. 返回值1. 0:TLS / SSL握手不成功,但被关闭,并受TLS / SSL协议规范的控制。使用返回值 ret 调用 SSL_get_error() 以找出原因2. 1:TLS/SSL 握手已成功完成,已建立 TLS/SSL 连接3. -1:TLS/SSL 握手不成功,因为在协议级别发生致命错误或发生连接故障。关闭不干净。如果需要操作以继续非阻塞 BIOS 的操作,也会发生这种情况。使用返回值 ret 调用 SSL_get_error() 以找出原因。- 数据收发处理
#include<openssl/ssl.h>intSSL_read(SSL *ssl,void*buf,int num);intSSL_write(SSL *ssl,constvoid*buf,int num)intSSL_get_error(const SSL *ssl,int ret);
1. SSL_read()1. 描述:将字节数从指定的 SSL 读取到缓冲区 buf 中2. 笔记:读取函数基于 SSL/TLS 记录工作。数据以记录形式接收(最大记录大小为 16kB)。只有当记录被完全接收到时,才能对其进行处理(解密和完整性检查)。因此,在上次读取调用时未检索到的数据仍然可以在 SSL 层内缓冲,并将在下一次读取调用时检索。如果 num 大于缓冲的字节数,则读取函数将返回缓冲的字节数。如果缓冲区中没有更多字节,则读取函数将触发下一条记录的处理。仅当记录被完全接收和处理时,读取函数才会返回报告成功。最多将返回一条记录的内容。由于 SSL/TLS 记录的大小可能超过底层传输(例如 TCP)的最大数据包大小,因此可能需要在记录完成并且读取调用成功之前从传输层读取多个数据包。3. 返回值1. > 大于0:读取操作成功。返回值是从 TLS/SSL 连接实际读取的字节数。2. > ≤0:读取操作未成功,因为连接已关闭、发生错误或调用进程必须执行操作。使用返回值 ret 调用 SSL_get_error(3) 以找出原因2. SSL_write()1. 描述:将缓冲区 buf 中的字节数写入指定的 SSL 连接2. 笔记:只有当写入长度为 num 的 buf 的完整内容时,写入函数才会成功返回。可以使用 SSL_CTX_set_mode(3) 的SSL_MODE_ENABLE_PARTIAL_WRITE选项更改此默认行为。设置此标志后,当部分写入成功完成时,写入函数也将返回成功。在这种情况下,写入函数操作被视为已完成。发送字节,并且必须启动具有新缓冲区的新写入调用(已删除已发送的字节)。部分写入以消息块的大小(16kB)执行3. 返回值1. > 大于0:写入操作成功,返回值是实际写入 TLS/SSL 连接的字节数2. > ≤0:写入操作未成功,因为连接已关闭、发生错误或调用进程必须执行操作。使用返回值 ret 调用 SSL_get_error() 以找出原因3. SSL_get_error()1. 描述:返回一个结果代码(适用于 C“switch”语句),用于前面对 SSL 上的 SSL_connect()、SSL_accept()、SSL_do_handshake()、SSL_read_ex()、SSL_read()、SSL_peek_ex()、SSL_peek()、SSL_shutdown()、SSL_write_ex() 或 SSL_write() 的调用。该 TLS/SSL I/O 函数返回的值必须传递给参数 ret 中的 SSL_get_error()2. 笔记:除了 ssl 和 ret,SSL_get_error() 还会检查当前线程的 OpenSSL 错误队列。因此,SSL_get_error() 必须在执行 TLS/SSL I/O 操作的同一线程中使用,并且不应在两者之间出现其他 OpenSSL 函数调用。在尝试 TLS/SSL I/O 操作之前,当前线程的错误队列必须为空,否则 SSL_get_error() 将无法可靠地工作3. 返回值:错误码描述SSL_ERROR_NONETLS/SSL I/O 操作已完成。当且仅当 ret > 0 时,才会返回此结果代码SSL_ERROR_ZERO_RETURNTLS/SSL 对等方已通过发送close_notify警报关闭了写入连接。无法读取更多数据。请注意,SSL_ERROR_ZERO_RETURN并不一定表示基础传输已关闭SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE操作未完成,以后可以重试SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT操作未完成;稍后应再次调用相同的 TLS/SSL I/O 函数。底层 BIO 尚未连接到对等体,调用将在 connect()/accept() 中阻塞。建立连接后,应再次调用 SSL 函数SSL_ERROR_WANT_X509_LOOKUP操作未完成,因为 SSL_CTX_set_client_cert_cb() 设置的应用程序回调已请求再次调用。稍后应再次调用 TLS/SSL I/O 函数。详细信息取决于应用程序。SSL_ERROR_WANT_ASYNC操作未完成,因为异步引擎仍在处理数据。仅当模式已设置为 SSL_MODE_ASYNC 使用 SSL_CTX_set_mode(3) 或 SSL_set_mode(3) 并且正在使用支持异步的引擎时,才会发生这种情况SSL_ERROR_WANT_ASYNC_JOB异步作业无法启动,因为池中没有可用的异步作业,仅当模式已使用 SSL_CTX_set_mode(3) 或 SSL_set_mode(3) 设置为 SSL_MODE_ASYNC,并且已通过调用 ASYNC_init_thread(3) 在异步作业池上设置了最大限制时,才会发生这种情况SSL_ERROR_WANT_CLIENT_HELLO_CB操作未完成,因为 SSL_CTX_set_client_hello_cb() 设置的应用程序回调已请求再次调用。稍后应再次调用 TLS/SSL I/O 函数。详细信息取决于应用程序SSL_ERROR_SYSCALL发生了一些不可恢复的致命 I/O 错误。OpenSSL 错误队列可能包含有关错误的详细信息SSL_ERROR_SSLSSL 库中发生不可恢复的致命错误,通常是协议错误。OpenSSL 错误队列包含有关错误的详细信息。如果发生此错误,则不应在连接上执行进一步的 I/O 操作,并且不得调用 SSL_shutdown()。 - 释放资源
#include<openssl/ssl.h>intSSL_shutdown(SSL *ssl);voidSSL_free(SSL *ssl);
1. SSL_shutdown()1. 描述:关闭活动的 TLS/SSL 连接。它将close_notify关闭警报发送到对等方;试向对等方发送close_notify关闭警报。无论操作是否成功,都会设置 SSL_SENT_SHUTDOWN 标志,并且当前打开的会话被视为已关闭且良好,并将保留在会话缓存中以供进一步重用。2. 笔记1. 第一个关闭连接:当应用程序是第一个发送close_notify警报的一方时,SSL_shutdown() 将仅发送警报,然后设置 SSL_SENT_SHUTDOWN 标志(以便会话被视为良好并将保留在缓存中)。如果成功,SSL_shutdown() 将返回 0。2. 对等方关闭连接:如果对等方已经发送了close_notify警报,并且已经在另一个函数中隐式处理了该警报(SSL_read(3)),则设置 SSL_RECEIVED_SHUTDOWN 标志。在这种情况下,SSL_read() 将返回 <= 0,SSL_get_error() 将返回 SSL_ERROR_ZERO_RETURN。SSL_shutdown() 将发送close_notify警报,设置 SSL_SENT_SHUTDOWN 标志。如果成功,SSL_shutdown() 将返回 1。3. 返回值1. 0:关闭过程正在进行中,尚未完成。对于 TLS 和 DTLS,这意味着已发送close_notify警报,但对等方尚未依次回复自己的close_notify。2. 1:关闭已成功完成。对于 TLS 和 DTLS,这意味着已发送close_notify警报,并收到对等方的close_notify警报。3. <0:关闭未成功。使用返回值 ret 调用 SSL_get_error(3) 以找出原因。如果需要操作来继续非阻塞 BIOS 的操作,则可能会发生这种情况。当并非所有数据都使用 SSL_read() 读取时,也会发生这种情况。2. SSL_free()1. 描述:SSL_free() 递减 SSL 的引用计数,并删除 SSL 指向的 SSL 结构,如果引用计数达到 0,则释放分配的内存。如果 ssl 为空,则不执行任何操作2. 笔记:SSL_free() 还调用间接受影响的项目的 free()ing 过程(如果适用):缓冲 BIO、读写 BIO、专门为此 SSL 创建的密码列表、SSL_SESSION。不要在调用 SSL_free() 之前或之后显式释放这些间接释放的项目,因为尝试释放两次可能会导致程序失败3. 返回值:无
03 | 实现思路
服务器处理流程
根据上面对官方服务器端例子的源码学习,大致了解了最基础的服务端处理流程,框图如下
从图中可以看出,与平常所学所见的
socket
程序相比,
OPENSSL
的服务器端多了三个步骤
- 通讯握手环节创建
socket
前,先进行了OPENSSL
库初始化,然后校验证书和私钥,也就是多了一个校验证书的环节 - 关联环节把创建的
SSL
实例与创建的socket
进行关联,使得后续的数据通信可以使用OPENSSL
提供的加解密处理的数据收发函数 - 释放资源最后在关闭释放
socket
句柄资源之前,需要先进行SSL
资源的断连和释放,因为前面创建的SSL
实例是全局性的
客户端处理流程
与服务器端流程类似,都是先初始化
OPENSSL
库,实例化
SSL
后,与创建的
socket
关联,再将客服端与服务器进行连接,最后通过
OPENSSL
的加解密处理的收发函数进行数据传输,在需要断开连接的时候,也是先断开
SSL
的连接,释放
SSL
的资源后再释放
socket
的句柄,流程框图如下
04 | 代码实现
理论知识有了一定的了解,那么最后需要通过实践操作来检验自己所学的知识是否能够应用到实际中。
之前学习过的Telnet协议,实现过简单的Telnet服务,现在想把SSL/TLS协议和Telnet协议搭配起来,实现一个TelnetS(Telnet + SSL/TLS)协议
服务端代码
在官方的服务端例程上进行了一点简单的修改(其实就是多加了点标注和交互消息打印,方便查看交互过程而已)
#include<unistd.h>#include<sys/socket.h>#include<arpa/inet.h>#include<openssl/ssl.h>#include<openssl/err.h>#include<string>#include<iostream>#include<chrono>#include<csignal>#include<thread>
using namespace std;/****************************************
* 函数名称:Pikashu_ReuseAddrPort
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:设置 socket fd 描述符的属性,打开地址、端口复用功能
* 参 数:设置属性的 socket fd
* 返 回 值:0: success | -1: SO_REUSEADDR failed | -2: SO_REUSEPORT failed
****************************************/intPikashu_ReuseAddrPort(int socketfd){int Reuse =1;if(0>setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&Reuse,sizeof(Reuse))){printf("SO_REUSEADDR failed: %s",strerror(errno));return-1;}
Reuse =1;if(0>setsockopt(socketfd, SOL_SOCKET, SO_REUSEPORT,&Reuse,sizeof(Reuse))){BC_LOGE("SO_REUSEPORT failed: %s",strerror(errno));return-2;}return0;}/****************************************
* 函数名称:Pikashu_CreateSocket
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:创建一个监听的socket
* 参 数:listenPort: 监听端口
* 返 回 值:创建的 socket fd | -1: socket failed | -2: Reuse failed | -3: bind failed | -4: listen failed
****************************************/intPikashu_CreateSocket(int listenPort){int sockFd =0;
sockFd =socket(AF_INET, SOCK_STREAM,0);if(sockFd <0){printf("create socket error: %d", errno);return-1;}// SO_REUSEADDR && SO_REUSEPORTif(0>Pikashu_ReuseAddrPort(sockFd)){printf("setsockopt error: %d", errno);close(sockFd);return-2;}structsockaddr_in Server_addr{};bzero(&Server_addr,sizeof(Server_addr));
Server_addr.sin_family = AF_INET;
Server_addr.sin_port =htons(listenPort);
Server_addr.sin_addr.s_addr =htonl(INADDR_ANY);bzero(&(Server_addr.sin_zero),8);if(0>bind(sockFd,(structsockaddr*)&Server_addr,sizeof(Server_addr))){printf("bind port = [%d], failure: %s\n", listenPort,strerror(errno));return-3;}// 限制开启连接数量, 系统分配:SOMAXCONN、自定义:5if(0>listen(sockFd,5)){printf("listen port = [%d], failure: %s\n",listenPort,strerror(errno));return-4;}return sockFd;}/****************************************
* 函数名称:Pikashu_InitOpenSSL
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:全局初始化openssl库,只需要调用一次
* 参 数:NULL
* 返 回 值:NULL
****************************************/voidPikashu_InitOpenSSL(){SSL_library_init();// SSL库初始化SSL_load_error_strings();// 载入所有SSL 错误消息OpenSSL_add_all_algorithms();// 加载所有支持的算法}/****************************************
* 函数名称:Pikashu_CleanOpenSSL
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:退出前清理openssl
* 参 数:NULL
* 返 回 值:NULL
****************************************/voidPikashu_CleanOpenSSL(){EVP_cleanup();}/****************************************
* 函数名称:Pikashu_CreateText
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:创建一个全局SSL_CTX,存储证书等信息
* 参 数:NULL
* 返 回 值:ctx: success | -1: failed
****************************************/
SSL_CTX *Pikashu_CreateText(){const SSL_METHOD *method;
SSL_CTX *ctx;/* 以SSL V2 和 V3 标准兼容方式产生一个SSL_CTX ,即SSL Content Text *//* 也可以用SSLv2_server_method() 或SSLv3_server_method() 单独表示V2 或V3 标准*/// method = SSLv3_server_method();// method = SSLv23_server_method();
method =TLS_server_method();
ctx =SSL_CTX_new(method);if(!ctx){perror("Unable to create SSL context");ERR_print_errors_fp(stderr);return-1;}return ctx;}/****************************************
* 函数名称:Pikashu_ConfigureContext
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:设置证书
* 参 数:ctx: SSL上下文 && certPath: 证书文件 && privateKeyPath: 私钥文件
* 返 回 值:NULL
****************************************/voidPikashu_ConfigureContext(SSL_CTX *ctx, string certPath, string privateKeyPath){SSL_CTX_set_ecdh_auto(ctx,1);// 载入用户的数字证书, 此证书用来发送给客户端。证书里包含有公钥if(0>=SSL_CTX_use_certificate_file(ctx, certPath.c_str()/*"cert.pem"*/, SSL_FILETYPE_PEM)){ERR_print_errors_fp(stderr);exit(EXIT_FAILURE);}// 载入用户私钥if(0>=SSL_CTX_use_PrivateKey_file(ctx, privateKeyPath.c_str()/*"key.pem"*/, SSL_FILETYPE_PEM)){ERR_print_errors_fp(stderr);exit(EXIT_FAILURE);}// 检查用户私钥是否正确if(!SSL_CTX_check_private_key(ctx)){ERR_print_errors_fp(stdout);exit(EXIT_FAILURE);}}/****************************************
* 函数名称:Pikashu_CheckOpensslError
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:检查OPENSSL产生的错误,并分析错误码
* 参 数:ssl: SSL实例 && retCode: SSL_read/SSL_write返回值 && isError: 是否确实发生了错误
* 返 回 值:NULL
****************************************/voidPikashu_CheckOpensslError(SSL *ssl,int retCode, bool &isError){// 处理ssl的错误码int sslErr =SSL_get_error(ssl, retCode);
isError = true;switch(sslErr){case SSL_ERROR_WANT_READ:{
cout <<"SSL_ERROR_WANT_READ"<< endl;
isError = false;break;}case SSL_ERROR_WANT_WRITE:{
cout <<"SSL_ERROR_WANT_WRITE"<< endl;
isError = false;break;}case SSL_ERROR_NONE:// 没有错误发生,这种情况好像没怎么遇到过{
cout <<"SSL_ERROR_WANT_WRITE"<< endl;break;}case SSL_ERROR_ZERO_RETURN:// == 0 ,代表对端关闭了连接{
cout <<"SSL remote close the connection"<< endl;break;}case SSL_ERROR_SSL:{
cout <<"SSL error:"<< sslErr << endl;break;}default:{
cout <<"SSL unknown error:"<< sslErr << endl;break;}}}/****************************************
* 函数名称:Pikashu_ClientMsgHandle
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:接收客户端连接,消息操作
* 参 数:socketFd: 客户端的socket文件句柄 && ctx:全局的上下文,保存有证书信息等
* 返 回 值:NULL
****************************************/voidPikashu_ClientMsgHandle(int socketFd, SSL_CTX *ctx){
cout <<"new connection coming"<< endl;
SSL *ssl;constchar reply[]="test\n";// 基于ctx 产生一个新的SSL
ssl =SSL_new(ctx);// 将连接用户的socket 加入到SSLSSL_set_fd(ssl, socketFd);auto t1 = chrono::steady_clock::now();// 建立SSL 连接int ret =SSL_accept(ssl);if(0< ret){
cout <<"ssl handshake success"<< endl;auto t2 = chrono::steady_clock::now();auto timeSpan = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
cout <<"SSL_accept cost "<< timeSpan.count()*1000<<" ms."<< endl;while(true){char tempBuf[512]={};int recvLen =SSL_read(ssl, tempBuf,sizeof(tempBuf));if(0< recvLen){
cout <<"客户端发来数据, len = "<< recvLen <<",content = "<< tempBuf << endl;// echo
cout <<"SSL_write "<<string(tempBuf, recvLen)<< endl;
ret =SSL_write(ssl, tempBuf, recvLen);if(0>= ret){
cout <<"SSL_write return <= 0, ret = "<< recvLen << endl;
bool isError = false;Pikashu_CheckOpensslError(ssl, recvLen, isError);if(isError){
cout <<"SSL_write error, close"<< endl;break;}}}else{// SSL_read <= 0 ,进一步检查openssl 的错误码,判断具体原因
cout <<"SSL_read return <= 0, ret = "<< recvLen << endl;
bool isError = true;Pikashu_CheckOpensslError(ssl, recvLen, isError);if(isError){
cout <<"SSL_read error,close"<< endl;break;}}/* TCP处理的流程,针对openssl,还需进一步针对 <= 0进行判断
* else if (recvLen == 0)
{
cout << "客户端主动断开连接,退出接收流程" << endl;
break;
}
else
{
cout << "发生其他错误, no = " << errno << ", desc = " << strerror(errno) << endl;
}*/}}else{int code =SSL_get_error(ssl, ret);auto reason =ERR_reason_error_string(code);if(code == SSL_ERROR_SYSCALL){
cout <<"ssl handshake error: errno = "<< errno <<", reason: "<<strerror(errno)<< endl;}else{
cout <<"ssl handshake error: code = "<< code <<", reason: "<< reason << endl;}ERR_print_errors_fp(stderr);}
cout <<"cleanup ssl connection"<< endl;// 关闭SSL 连接SSL_shutdown(ssl);// 释放SSLSSL_free(ssl);// 关闭socketclose(socketFd);}intmain(){int sockFd;
SSL_CTX *ctx;// 捕获SIG_IGN信号,解决Broken pipe导致进程崩溃问题signal(SIGPIPE, SIG_IGN);Pikashu_Init_OpenSSL();
ctx =Pikashu_CreateText();Pikashu_ConfigureContext(ctx,"../ssl/google.com.pem","../ssl/google.com.key");
cout <<"listen at :1688"<< endl;
sockFd =Pikashu_CreateSocket(1688);/* Handle connections */while(true){structsockaddr_in addr{};socklen_t len =sizeof(addr);// 阻塞,直到有新的连接到来int clientFd =accept(sockFd,(structsockaddr*)&addr,&len);if(0> clientFd){perror("Unable to accept\n");break;}// 单独起1个线程处理客户端逻辑(错误的用法,这里只是为了演示,实战中需要使用epoll多路复用技术)
thread task(Pikashu_ClientMsgHandle, clientFd, ctx);
task.detach();}// 关闭监听socket文件句柄close(sockFd);// 退出前释放全局的上下文SSL_CTX_free(ctx);// 清理opensslPikashu_CleanOpenSSL();return0;}
客户端代码
仿照服务端的例程,按照客户端的流程写的一个简单交互客户端,略微丑陋
#include<openssl/ssl.h>#include<openssl/err.h>#include<netinet/in.h>#include<arpa/inet.h>#include<unistd.h>#include<sys/socket.h>#include<resolv.h>#include<iostream>#include<thread>
using namespace std;/****************************************
* 函数名称:Pikashu_ShowCerts
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:j解析显示整数内容
* 参 数:ssl
* 返 回 值:NULL
****************************************/voidPikashu_ShowCerts(SSL *ssl){
X509 *cert;char*line;
cert =SSL_get_peer_certificate(ssl);if(nullptr != cert){
cout <<"数字证书信息: "<< endl;
line =X509_NAME_oneline(X509_get_subject_name(cert),0,0);
cout <<"证书: "<< line << endl;free(line);
line =X509_NAME_oneline(X509_get_issuer_name(cert),0,0);
cout <<"颁发者: "<< line << endl;free(line);X509_free(cert);}else{
cout <<"无证书信息!"<< endl;}}/****************************************
* 函数名称:Pikashu_InitOpenSSL
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:全局初始化openssl库,只需要调用一次
* 参 数:NULL
* 返 回 值:NULL
****************************************/voidPikashu_InitOpenSSL(){SSL_library_init();// SSL库初始化SSL_load_error_strings();// 载入所有SSL 错误消息OpenSSL_add_all_algorithms();// 加载所有支持的算法}/****************************************
* 函数名称:Pikashu_CreateText
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:创建一个全局SSL_CTX,存储证书等信息
* 参 数:NULL
* 返 回 值:ctx: success | -1: failed
****************************************/
SSL_CTX *Pikashu_CreateText(){const SSL_METHOD *method;
SSL_CTX *ctx;/* 以SSL V2 和 V3 标准兼容方式产生一个SSL_CTX ,即SSL Content Text *//* 也可以用SSLv2_server_method() 或SSLv3_server_method() 单独表示V2 或V3 标准*///method = SSLv3_server_method();
method =SSLv23_client_method();
ctx =SSL_CTX_new(method);if(!ctx){perror("Unable to create SSL context");ERR_print_errors_fp(stderr);exit(EXIT_FAILURE);}return ctx;}/****************************************
* 函数名称:Pikashu_CreateSocket
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:创建一个监听的socket
* 参 数:serverIp: 服务器ip地址 && serverPort: 服务器端口
* 返 回 值:创建的 socket fd | -1: socket failed | -2: Reuse failed | -3: bind failed | -4: listen failed
****************************************/intPikashu_CreateSocket(string serverIp,uint16_t serverPort){int sockFd =0;structsockaddr_in Client_addr{};bzero(&Client_addr,sizeof(Client_addr));
Client_addr.sin_family = AF_INET;
Client_addr.sin_port =htons(serverPort);
Client_addr.sin_addr.s_addr =inet_addr(serverIp.c_str());bzero(&(Client_addr.sin_zero));
sockFd =socket(PF_INET, SOCK_STREAM,0);if(0> sockFd){printf("create socket error: %d", errno);exit(EXIT_FAILURE);}int ret =connect(sockFd,(structsockaddr*)&Client_addr,sizeof(sockaddr_in));if(0!= ret){
cout <<"Connect err: "<< errno << endl;exit(errno);}return sockFd;}/****************************************
* 函数名称:Pikashu_CheckOpensslError
* 作 者:Pikashu
* 设计日期:2023-06-15
* 功能描述:检查OPENSSL产生的错误,并分析错误码
* 参 数:ssl: SSL实例 && retCode: SSL_read/SSL_write返回值 && isError: 是否确实发生了错误
* 返 回 值:NULL
****************************************/voidPikashu_CheckOpensslError(SSL *ssl,int retCode, bool &isError){// 处理ssl的错误码int sslErr =SSL_get_error(ssl, retCode);
isError = true;switch(sslErr){case SSL_ERROR_WANT_READ:{
cout <<"SSL_ERROR_WANT_READ"<< endl;
isError = false;break;}case SSL_ERROR_WANT_WRITE:{
cout <<"SSL_ERROR_WANT_WRITE"<< endl;
isError = false;break;}case SSL_ERROR_NONE:// 没有错误发生,这种情况好像没怎么遇到过{
cout <<"SSL_ERROR_WANT_WRITE"<< endl;break;}case SSL_ERROR_ZERO_RETURN:// == 0 ,代表对端关闭了连接{
cout <<"SSL remote close the connection"<< endl;break;}case SSL_ERROR_SSL:{
cout <<"SSL error:"<< sslErr << endl;break;}default:{
cout <<"SSL unknown error:"<< sslErr << endl;break;}}}intmain(){
SSL_CTX *ctx = nullptr;// 初始化opensslPikashu_InitOpenSSL();
cout <<"init openssl success"<< endl;// 初始化socket,同步连接远端服务器int socketFd =Pikashu_CreateSocket("10.80.0.17",1688);
cout <<"tcp connect remote success"<< endl;// 创建SSL_CTX上下文
ctx =Pikashu_CreateText();// 绑定socket句柄到SSL实例上
SSL *ssl =SSL_new(ctx);SSL_set_fd(ssl, socketFd);// 建立SSL链接,握手
cout <<"SSL_connect 2s later will connect and do hand shake..."<< endl;
this_thread::sleep_for(chrono::seconds(2));
cout <<"SSL_connect "<< endl;int ret =SSL_connect(ssl);if(0>= ret){ERR_print_errors_fp(stderr);return0;}
cout <<"handshake success"<< endl;// 显示对方证书信息
cout <<"Connected with "<<SSL_get_cipher(ssl)<<" encryption"<< endl;Pikashu_ShowCerts(ssl);
cout <<"send hello server"<< endl;
string msg ="hello serve";SSL_write(ssl, msg.c_str(), msg.length());// wait server responsechar tempBuf[256]={};
ret =SSL_read(ssl, tempBuf,sizeof(tempBuf));if(0>= ret){
cout <<"SSL_read return <=0,ret="<< ret << endl;
bool isError = false;Pikashu_CheckOpensslError(ssl, ret, isError);if(isError){
cout <<"SSL_read error,close"<< endl;}}
this_thread::sleep_for(chrono::seconds(5));
cout <<"exit ..."<< endl;SSL_shutdown(ssl);// 关闭SSL连接SSL_free(ssl);// 释放SSL资源close(socketFd);// 关闭socket文件句柄SSL_CTX_free(ctx);// 释放SSL_CTX上下文资源return0;}
编译过程 & 执行结果
- 对
openssl_xxx
函数未定义的引用
- 问题原因:很常见的问题,没有找到
OPENSSL
的库函数 - 解决方法:编译的时候添加静态链接
-lssl -lcrypto
undefined reference to symbol 'Pthread_create@GLIBC_2.2.5'
- 问题原因:
pthread
不是linux
下的默认的库,在链接的时候,无法找到phread
库中线程函数的入口地址,所以链接失败 - 解决方法:编译的时候添加静态链接
-lpthread -lm
- 最终编译完成
- 执行结果
版权归原作者 Ltd Pikashu 所有, 如有侵权,请联系我们删除。