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

news2024/11/17 12:28:05

文章目录

  • 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. 执行结果

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SSM框架搭建

SSM环境搭建 1 IDE的话&#xff0c;我用的MyEcplise&#xff0c;如果用Ecplise的话&#xff0c;需要自行配置服务器。 首先&#xff0c;建一个Dynamic工程&#xff0c;需要注意的是一定要勾选上web.xml。 将Spring框架包、jstl包、standard包、common-logging包、aopallian…

系列十三、MongoDB聚合查询

一、概述 MongoDB聚合框架&#xff08;Aggregation Framework&#xff09;是一个计算框架&#xff0c;它可以&#xff1a; ①&#xff1a;作用在一个或者几个集合上; ②&#xff1a;对集合中的数据进行一系列的运算; ③&#xff1a;将这些数据转化为期望的形式; 从效果而言…

AIGC技术研究与应用 ---- 下一代人工智能:新范式!新生产力!(5 - AIGC 未来展望)

文章大纲 不可避免的职业替代AI 对人类思维的影响AIGC 的风险人工智能对齐 -- 价值学习 鲁棒适应参考文献与学习路径GPT 系列模型解析前序文章模型进化券商研报陆奇演讲多模态据预测,未来五年10%-30%的图片内容由AI参与生成,考虑到下一代互联网对内容需求的迅速提升,2030年A…

计算机网络管理 实验4(一) SNMP报文分析之验证SNMP协议的工作过程以及分析SNMP数据单元的格式

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;(*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &am…

FPGA基础知识-门级建模

目录 学习目标 学习内容 1.门的类型 2.门延迟 学习时间 学习小结 学习目标 学习Verilog 提供的门级原语 理解门的实例引用、门的符号以及andor&#xff0c;bufnot类型的门的真值表 学习如何根据电路的逻辑图来生成verilog描述 讲述门级设计中的上升、下降和关断延迟 …

【论文解读系列】Blip-2:引导语言图像预训练具有冻结图像编码器和大型语言模型

Blip-2 BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models BLIP-2&#xff1a;引导语言图像预训练具有冻结图像编码器和大型语言模型 (0) 总结&实测 总结&#xff1a;blip-2 最大的贡献在于&#xff0c;提出了…

跨境电商社交媒体:选择合适的平台

正如您在使用社交媒体的九种方法中了解到的那样&#xff0c;跨境电商优先考虑社交媒体营销可能会更有利可图。有数十个平台可供选择&#xff0c;每个平台都迎合了具有不同兴趣、特征和位置的独特受众。 那么您应该关注哪个社交媒体平台以及如何以最大效率工作&#xff1f; 目…

【Vue全家桶高仿小米商城】——(三)VueCli4.0安装和使用

文章目录 第三章&#xff1a;VueCli4.0安装和使用一、VueCli4.0安装&#xff1a;二、创建Vue项目方式一 使用脚本搭建方式二 使用UI界面搭建 三、安装依赖方式一 Vue脚手架图形界面安装方式二、命令行安装 四、Vue DevTools安装使用两种安装方式&#xff1a; 第三章&#xff1a…

Linux权限解析

一、Linux用户分类 Linux下有两种用户&#xff1a;超级用户&#xff08;root&#xff09;、普通用户 超级用户&#xff1a;可以再linux系统下做任何事情&#xff0c;不受限制 普通用户&#xff1a;在linux下做有限的事情。 超级用户的命令提示符是“#”&#xff0c;普通用户的…

6.6面向对象继承

2. 面向对象特征二&#xff1a;继承(Inheritance) 2.1 继承的概述 说到继承我们会想到什么 在Java面向对象程序设计中的继承&#xff0c;多个类中存在相同属性和行为时&#xff0c;将这些相同的内容抽取到单独一个类&#xff08;父类&#xff09;中&#xff0c;然后所有的类继…

【备战秋招】每日一题:4月15日美团春招第二题:题面+题目思路 + C++/python/js/Go/java带注释

为了更好的阅读体检&#xff0c;为了更好的阅读体检&#xff0c;&#xff0c;可以查看我的算法学习博客第二题-分糖果 在线评测链接:P1236 题目内容 某天&#xff0c;塔子哥去商店买了两种不同口味的糖果&#xff0c;分别买了 a 个和 b 个。当他回到家时&#xff0c;他发现他…

IDE/以glog为例实践CMake-Gui工具使用

文章目录 概述构建glog编译glog.sln解决方案glog的配置项BUILD_SHARED_LIBSBUILD_TESTINGCMAKE_CONFIGURATION_TYPESCMAKE_INSTALL_PREFIXWITH_GTESTGTest_DIRWITH_GFLAGSgflag_DIRPRINT_UNSYMBOLIZED_STACK_TRACESWITH_SYMBOLIZEWITH_FUZZINGWITH_THREADSWITH_TLSWITH_GMOCKWI…

3分钟了解如何做好项目管理和协作

目录 介绍 什么是项目管理 项目管理三要素 时间 成本 质量 项目启动阶段项目启动负责人要了解四个问题 项目规划阶段 任务分解->任务优先级安排->计划呈现->风险控制 快速呈现项目工作计划 项目启动&#xff1a;信息同步任务分工高效 项目跟踪&#xff1a…

实战:用docker-compose容器化springboot项目

文章目录 前言技术积累docker-compose定义docker-compose文件参数docker-compose命令 实战演示1、创建挂载路径2、编写docker-compose.yml3、启动并管理容器 写在最后 前言 前面我们学习和实战了用dockerfile构建镜像&#xff0c;通过镜像可以任意在docker环境容器化部署项目。…

C++静态联编和动态联编

目录 2.1静态联编 2.2动态联编 2.3虚函数面试题 2.3.1构造函数中使用memset函数 2.3.2this指针与虚函数的调用 2.3.3构造析构函数中调用虚函数 2.3.4动态和静态联编与访问属性和默认值 2.3.5动态创建对象时的析构函数 联编是指计算机程序彼此关联的过程&#xff0c;是把…

Fiddler汉化成功

我安装的fiddler 操作系统是&#xff1a;Win10 64Bit 操作系统的版本号是&#xff1a;v5.0.20194.41348 for .NET 4.6.1 fiddler下载地址&#xff1a; 我用夸克网盘分享了「02-Web调试工具-FiddlerSetup.exe」&#xff0c;点击链接即可保存。 链接&#xff1a;https://pan.quar…

什么是项目里程碑?如何为项目成功设置?

高速公路上每隔一公里就有一个标志牌&#xff0c;这表明你需要进一步行驶才能到达目的地的距离。虽然没有这些标志你也可以到达目的地&#xff0c;但它们的存在使你放心&#xff0c;让你确信走在正确的道路上。 项目里程碑在项目管理中也有同样的作用。当你的项目实现目的时&a…

Linux内核学习----整体概览

目录 1、概述 2、核心抽象及设计选型 2.1. 对进程和内核的抽象 2.2. 对进程地址空间的抽象 2.3. 支持可重入可抢占的内核 2.4. 放松管控与努力回收 2.5. 单块结构内核动态加载模块 2.6. 为系统中的一切活动打拍子 2.7. 一切皆文件的理念 3、Linux整体架构模块说明 3.…

对C++中const的说明

对C中const的说明 在C中&#xff0c;const是一个关键字&#xff0c;用于指定对象或变量是只读的&#xff0c;即不可修改。它可以应用于不同的上下文中&#xff0c;包括&#xff1a; 对象和变量声明&#xff1a;通过在变量或对象的声明前加上const关键字&#xff0c;可以将其标…

ACL2022 Document-Level Event Argument Extraction via Optimal Transport

Document-Level Event Argument Extraction via Optimal Transport 论文&#xff1a;https://aclanthology.org/2022.findings-acl.130/ 代码&#xff1a;- 期刊/会议&#xff1a;ACL 2022 摘要 事件论元抽取&#xff08;EAE&#xff09;是事件抽取的子任务之一&#xff0c…