计算机网络基础知识(九)—— 什么是TelnetS?Telnet Over TLS

文章目录

  • 01 | TelnetS
  • 02 | OpenSSL
  • 03 | 实现思路
    • 服务器处理流程
    • 客户端处理流程
  • 04 | 代码实现
    • 服务端代码
    • 客户端代码
    • 编译过程 & 执行结果

前面学习了什么是HTTPS协议,了解了HTTPS的工作原理以及具体的工作流程,了解了HTTP协议和HTTPS协议之间的区别。当然,纸上学来终觉浅,懂了原理还不行,还是得实际操作一遍才能真正的理解其工作流程。
下面通过之前所学的Telnet协议,HTTPS协议知识,结合起来进行实操练习,给自己一个目标,就是说实现一个安全性的Telnet服务 —— TelnetS服务

01 | TelnetS

HTTPS = HTTP + SSL/TLS

在网络编程中,主要体现到增加了证书校验,传输加密的过程

02 | OpenSSL

www.openssl.org

加密算法有很多,这里主要通过OPENSSL提供的API以示例代码进行学习

OPENSSL是开源的安全的套接字层的数据传输加密库,主要提供了多种加密算法、密钥/证书管理、SSL协议等功能,整体开源包分为三个部分:libssl, libcrypto, openssl

  • libssl:SSL协议库

  • libcrypto:加密算法库

  • openssl:总体app命令工具(函数)库

通过官方的服务端例子源码进行学习,其中关键函数意义如下:

wiki.openssl.org

  1. 初始化 OPENSSL

    以下函数在OPENSSL的V1.1.0版本开始被OPENSSL_init_ssl()弃用

    #include <openssl/ssl.h>
    
    int SSL_library_init(void);
    void SSL_load_error_strings(void);
    int OpenSSL_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”

  2. 加载 & 校验证书

    #include <openssl/ssl.h>
    
    /* 描述:
    * 
    * 笔记:在进行任何其他操作之前,必须先调用该函数,且不可重复调用
    * 
    */
    SSL_CTX *SSL_CTX_new(const SSL_METHOD *method);
    
    
    int SSL_use_certificate_file(SSL *ssl, const char *file, int type);
    int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
    int SSL_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。否则,请检查错误堆栈以找出原因

  3. 创建与 socket 绑定的 SSL 实例

    #include <openssl/ssl.h>
    
    SSL *SSL_new(SSL_CTX *ctx);
    int SSL_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 以在 SSLfd 之间进行接口。BIO和SSL引擎继承了fd的行为。如果 fd 是非阻塞的,则 SSL 也将具有非阻塞行为。如果已经有一个BIO连接到ssl,将调用BIO_free()(对于读取和写入端,如果不同)

      2. 返回值

        1. 0:操作失败。检查错误堆栈以找出原因;

        2. 1:操作成功

  4. SSL 握手,建立 SSL 连接

    #include <openssl/ssl.h>
    
    int SSL_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_READSSL_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() 以找出原因。

  5. 数据收发处理

    #include <openssl/ssl.h>
    
    int SSL_read(SSL *ssl, void *buf, int num);
    int SSL_write(SSL *ssl, const void *buf, int num)
    int SSL_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. 笔记:只有当写入长度为 numbuf 的完整内容时,写入函数才会成功返回。可以使用 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. 笔记:除了 sslret,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()。
  6. 释放资源

    #include <openssl/ssl.h>
    
    int SSL_shutdown(SSL *ssl);
    void SSL_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 的服务器端多了三个步骤

  1. 通讯握手环节

    创建 socket 前,先进行了 OPENSSL 库初始化,然后校验证书和私钥,也就是多了一个校验证书的环节

  2. 关联环节

    把创建的 SSL 实例与创建的 socket 进行关联,使得后续的数据通信可以使用 OPENSSL 提供的加解密处理的数据收发函数

  3. 释放资源

    最后在关闭释放 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
 ****************************************/
int Pikashu_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;
    }
    return 0;
}

/****************************************
 * 函数名称:Pikashu_CreateSocket
 * 作    者:Pikashu
 * 设计日期:2023-06-15
 * 功能描述:创建一个监听的socket
 * 参    数:listenPort: 监听端口
 * 返 回 值:创建的 socket fd | -1: socket failed | -2: Reuse failed | -3: bind failed | -4: listen failed
 ****************************************/
int Pikashu_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_REUSEPORT
    if (0 > Pikashu_ReuseAddrPort(sockFd))
    {
        printf("setsockopt error: %d", errno);
        close(sockFd);
        return -2;
    }

    struct sockaddr_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, (struct sockaddr *) &Server_addr, sizeof(Server_addr)))
    {
        printf("bind port = [%d], failure: %s\n", listenPort, strerror(errno));
        return -3;
    }

    // 限制开启连接数量, 系统分配:SOMAXCONN、自定义:5
    if (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
 ****************************************/
void Pikashu_InitOpenSSL()
{
    SSL_library_init();             // SSL库初始化
    SSL_load_error_strings();       // 载入所有SSL 错误消息
    OpenSSL_add_all_algorithms();   // 加载所有支持的算法
}

/****************************************
 * 函数名称:Pikashu_CleanOpenSSL
 * 作    者:Pikashu
 * 设计日期:2023-06-15
 * 功能描述:退出前清理openssl
 * 参    数:NULL
 * 返 回 值:NULL
 ****************************************/
void Pikashu_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
 ****************************************/
void Pikashu_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
 ****************************************/
void Pikashu_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
 ****************************************/
void Pikashu_ClientMsgHandle(int socketFd, SSL_CTX *ctx)
{
    cout << "new connection coming" << endl;
    SSL *ssl;
    const char reply[] = "test\n";
    // 基于ctx 产生一个新的SSL
    ssl = SSL_new(ctx);
    // 将连接用户的socket 加入到SSL
    SSL_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);
    // 释放SSL
    SSL_free(ssl);
    // 关闭socket
    close(socketFd);
}

int main()
{
    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)
    {
        struct sockaddr_in addr{};
        socklen_t len = sizeof(addr);
        // 阻塞,直到有新的连接到来
        int clientFd = accept(sockFd, (struct sockaddr *) &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);
    // 清理openssl
    Pikashu_CleanOpenSSL();
    return 0;
}

客户端代码

仿照服务端的例程,按照客户端的流程写的一个简单交互客户端,略微丑陋

#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
 ****************************************/
void Pikashu_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
 ****************************************/
void Pikashu_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
 ****************************************/
int Pikashu_CreateSocket(string serverIp, uint16_t serverPort)
{
    int sockFd = 0;
    struct sockaddr_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, (struct sockaddr *) &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
 ****************************************/
void Pikashu_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;
            }
    }
}

int main()
{
    SSL_CTX *ctx = nullptr;
    // 初始化openssl
    Pikashu_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);
        return 0;
    }

    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 response
    char 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上下文资源
    return 0;
}

编译过程 & 执行结果

  1. openssl_xxx 函数未定义的引用

在这里插入图片描述

  • 问题原因:很常见的问题,没有找到 OPENSSL 的库函数

  • 解决方法:编译的时候添加静态链接 -lssl -lcrypto

  1. undefined reference to symbol 'Pthread_create@GLIBC_2.2.5'

在这里插入图片描述

  • 问题原因:pthread 不是 linux 下的默认的库,在链接的时候,无法找到 phread 库中线程函数的入口地址,所以链接失败

  • 解决方法:编译的时候添加静态链接 -lpthread -lm

  1. 最终编译完成

在这里插入图片描述

  1. 执行结果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/31546.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch 安装(Linux)

ElasticSearch 概念安装安装es 后台启动 & 停止启动nohup 记录pid 停止 其他启动错误max number of threads内存不足 Cannot allocate memoryfailed to obtain node locks 概念 ES是一款分布式全文搜索引擎&#xff0c;基于Lucene&#xff0c;进行了二次封装&#xff0c;更…

ElasticSearch-安装Head可视化插件

安装Head可视化插件 首先需要依赖node.js和npm环境 1 安装node.js 官方下载地址:http://nodejs.cn/download/ 下载LTS版本&#xff08;长期稳定版本&#xff09; 安装可以更改安装路径,其余的都是选择 下一步傻瓜是安装 安装成功后如下 命令测试 node -v 查看node的版本 n…

Redis 2023面试5题(二)

一、Redis key没设置过期时间却被redis主动删除了 如果一个 Redis 键没有设置过期时间&#xff0c;那么 Redis 无法判断该键是否应该自动删除。因此&#xff0c;如果一个没有设置过期时间的键被 Redis 主动删除了&#xff0c;可能是以下原因之一&#xff1a; 内存不足&#x…

【论文笔记】BEIT:BERT PRE-TRAINING OF IMAGE TRANSFORMERS

GitHub 1.介绍 1.1 挑战 视觉转换器的输入单元&#xff0c;即图像补丁&#xff0c;没有预先存在的词汇。预测遮罩面片的原始像素往往会在预训练短程依赖性和高频细节上浪费建模能力 1.2 回顾一下Bert的基本架构和流程 输入编码&#xff1a;通过tokenizer将输入的文本中的每…

OceanBase—01(入门篇——使用docker安装OceanBase以及介绍连接OB的几种方式)

OceanBase—01&#xff08;入门篇——使用docker安装OceanBase以及介绍连接OB的几种方式&#xff09; 1. 前言1.1 安装部署参考1.1.1 安装前提1.1.2 参考 1.1 修改数据库用户名密码1.2 总结常见连接命令 2. 安装部署OceanBase2.1 启动 OceanBase 数据库实例2.1.1 拉取镜像并启动…

eclipse (C/C++) 常用设置记录

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台&#xff1b;现公司用其作为开发单片机的IDE&#xff1b;因此记录一下常用的配置方法&#xff1b; 文章目录 零、常用默认快捷键一、高亮相同变量二、修改高亮变量颜色三、在整个工程内搜索某个函数四、切换主题五、改变字…

编译原理笔记16:自下而上语法分析(3)构造 DFA、DFA 对下一步分析的指导(有效项目)

目录 由 NFA 用子集法构造 DFA由 LR(0) 项目直接构造识别活前缀的 DFA构造 DFA求拓广文法 GCLOSURE & GO例&#xff1a; 构造 DFA DFA 指导下一步分析有效项目 看了前面的内容&#xff0c;我们已经了解到&#xff1a;分析表和驱动器算法&#xff0c;是 LR 分析器的核心。 …

基于Java+Swing+Mysql实现图书管理系统V2.0

基于JavaSwingMysql实现图书管理系统V2.0 一、系统介绍二、功能展示1.项目内容2.项目骨架3.数据库表4.主界面5.添加6、修改7、查询8、删除 四、其它1.其他系统实现五.获取源码 一、系统介绍 本系统主要有对图书信息的增删改查操作功能。 项目类型&#xff1a;Java SE项目&…

linux 下查看 USB 设备

文章目录 前言目录内容详解usb11-0:1.01-1.1:1.0 结构图设备信息bDeviceClassversionbusnum & devnumdevbMaxPoweridVendor & idProductproductmanufacturerbcdDevicespeedueventbmAttributesdrivers_autoprobe 前言 在 sysfs 文件系统下&#xff0c;查看 USB 设备&am…

Android 9-SystemUI:(1)启动流程

具体分析(以下代码示例&#xff0c;讲解&#xff0c;都是通过&#xff0c;Android9代码来举例&#xff09; SystemUI,其实是可以看作是一个系统级的服务&#xff0c;也就是SystemUIService, SystemUI的服务启动&#xff0c;要从SystemServer.run()方法入手 main 方法里启动了…

企业级微服务架构实战项目--xx优选3-mq+nacos+es实现上下架

一 nacosmqes实现上下架 1.1 架构图 1.2 工程结构 1.3 核心代码流程 1.3.1 请求product模块 2.修改数据库&#xff0c;推送rabbitmq中 1.3.2 rabbitmq的工具类 1.3.3 search模块中rabbit客户端订阅信息 1.监听器监听信息 2.调用相应的上下架方法 2.1 调用product模块&…

Flink 学习三 Flink 流 process function API

Flink 学习三 Flink 流&process function API 1.Flink 多流操作 1.1.split 分流 (deprecated) 把一个数据流根据数据分成多个数据流 1.2 版本后移除 1.2.分流操作 (使用侧流输出) public class _02_SplitStream {public static void main(String[] args) throws Excep…

uniapp设置滚动条滚动到指定位置

场景&#xff1a;左侧菜单栏&#xff0c;每次切换时&#xff0c;需要右侧商品展示区保持滚动条及页面在最顶部 1.利用scroll-view 中scroll-top属性实现 1.1设置scrollToTop属性为0 data() {return {// 保证每次切换&#xff0c;滚动条位置都在最顶部scrollToTop: 0,}; } 1.…

ansible的剧本(playbook)

一、playbooks 概述以及实例操作 1、playbooks 的组成 playbooks 本身由以下各部分组成 &#xff08;1&#xff09;Tasks&#xff1a;任务&#xff0c;即通过 task 调用 ansible 的模板将多个操作组织在一个 playbook 中运行 &#xff08;2&#xff09;Variables&#xff1…

iOS 单元测试之常用框架 OCMock 详解 | 京东云技术团队

一、单元测试 1.1 单元测试的必要性 测试驱动开发并不是一个很新鲜的概念了。在日常开发中&#xff0c;很多时候需要测试&#xff0c;但是这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来的东西。测试的时候&#xff0c;往往是用模拟器一次一次的从头开始启动 app&a…

团体程序设计天梯赛-练习集L2篇④

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;Hello大家好呀&#xff0c;我是陈童学&#xff0c;一个与你一样正在慢慢前行的普通人。 &#x1f3c0;个人主页&#xff1a;陈童学哦CSDN &#x1f4a1;所属专栏&#xff1a;PTA &#x1f381;希望各…

模糊聚类在负荷实测建模中的应用(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

VSCode使用CodeWhisperer(AI编程)

安装AWS Toolkit插件&#xff0c;点击侧边插件搜索并安装 2.点击aws ->CodeWhisperer ->Start 3.在下拉菜单中点击Use a personal email to sign up and sign in with AWS Builder ID 4.点Copy Code and Proceed&#xff0c;这会自动复制一些东西 5. 收到提示打开外部网…

【跑实验05】利用CLIP中的图像编码器,如何遍历文件夹中的图像,将图像文件改为28*28的尺寸,然后输出到excel中的每一列,最后一列全都标记为0

文章目录 一、初步实现二、警告信息的解决三、Excel的限制四、尝试解决 一、初步实现 要遍历文件夹中的图像并将其尺寸调整为28x28&#xff0c;并将结果输出到Excel中&#xff0c;可以按照以下步骤进行操作&#xff1a; 首先&#xff0c;确保您已经安装了Pandas库&#xff0c…

“插入排序:小数据量排序的王者“

文章目录 &#x1f50d;什么是插入排序&#xff1f;&#x1f511;插入排序的优缺点&#x1f680;实现插入排序 &#x1f50d;什么是插入排序&#xff1f; 插入排序是一种简单的排序算法&#xff0c;它的基本思想是&#xff1a;将待排序的元素&#xff0c;从第二个元素开始&…