当前位置: 首页 > article >正文

编写一个基于OpenSSL的SSL/TLS服务端(HTTPS)可运行的完整示例

初级代码游戏的专栏介绍与文章目录-CSDN博客

我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。

这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。

源码指引:github源码指引_初级代码游戏的博客-CSDN博客

本文代码位于httpd目录


        现在不支持SSL/TLS已经不好意思了,原则上旧的代码仅仅需要增加安全连接建立过程然后将原来的接收发送的代码改成openssl的对应函数就可以了。但旧的代码如果写的太分散,处理连接和收发的代码到处都是,那就比较头疼了。

        下面介绍使用openssl的代码示例以及如何把我的socket和http代码如何修改成https。

目录

一、基本功能的分解和包装

1.1 OpenSSL包装类

1.2 初始化SSL_CTX

1.3 建立SSL连接SSL_accept

1.4 发送和接收SSL_write SSL_read

二、服务器的改造

2.1 服务器初始化

2.2 建立SSL连接

2.3 释放

三、测试代码

3.1 测试代码

3.2 配置、运行和结果


一、基本功能的分解和包装

1.1 OpenSSL包装类

        包装类很简单(mySSLTLS.h):

class CmySSLTLS
{
private:
    SSL_CTX* ctx;
    
    int _test_SSLTLS();
public:
    //初始化
    bool Init_SSL_CTX();

    //处理一个连接,以socket为参数
    SSL* getSSL(int sd);
    //释放SSL
    void freeSSL(SSL* ssl);

    //结束
    void free_SSL_CTX();
 
    //测试
    static int test_SSLTLS();
};

        SSL_CTX是上下文(类中唯一的成员变量),设置各种参数和所需的证书文件,SSL代表一个连接,参数是个文件描述符(socket也是文件描述符),SSL实际上与一个连接相关的SSL状态信息。SSL_CTX和SSL用完都要释放。

        最后有个测试函数用来验证。

1.2 初始化SSL_CTX

/*所有需要的参数信息都在此处以#define的形式提供*/
#define CERTF   "server.cer" /*服务端的证书(需经CA签名)*/
#define KEYF   "server.key"  /*服务端的私钥(建议加密存储)*/
#define CACERT "ca.cer" /*CA 的证书*/
#define PORT   60000   /*准备绑定的端口,注意只是测试程序test_SSLTLS使用*/

#define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(1); }

bool CmySSLTLS::Init_SSL_CTX()
{
	SSL_METHOD const* meth;

	SSL_library_init();
	SSL_load_error_strings();            /*为打印调试信息作准备*/
	OpenSSL_add_ssl_algorithms();        /*初始化*/
	meth = SSLv23_server_method();  /*采用什么协议(SSLv2/SSLv3/TLSv1)在此指定*/

	ctx = SSL_CTX_new(meth);
	if (ctx == NULL)
	{
		thelog << "SSL_CTX_new 失败" << ende;
		return false;
	}

	int mode = SSL_VERIFY_NONE;/*验证与否 SSL_VERIFY_NONE SSL_VERIFY_PEER */
	SSL_CTX_set_verify(ctx, mode, NULL);
	//加载证书
	{
		if (1 != SSL_CTX_load_verify_locations(ctx, CACERT, NULL)) /*若验证,则放置CA证书*/
		{
			ERR_print_errors_fp(stderr);
			thelog << "SSL_CTX_load_verify_locations 失败" << ende;
			return false;
		}

		if (1 != SSL_CTX_use_certificate_file(ctx, CERTF, SSL_FILETYPE_PEM))
		{
			ERR_print_errors_fp(stderr);
			thelog << "SSL_CTX_use_certificate_file 失败" << ende;
			return false;
		}
		if (1 != SSL_CTX_use_PrivateKey_file(ctx, KEYF, SSL_FILETYPE_PEM))
		{
			ERR_print_errors_fp(stderr);
			thelog << "SSL_CTX_use_PrivateKey_file 失败" << ende;
			return false;
		}

		if (1 != SSL_CTX_check_private_key(ctx))
		{
			printf("Private key does not match the certificate public key\n");
			thelog << "SSL_CTX_check_private_key 失败" << ende;
			return false;
		}
	}

	SSL_CTX_set_cipher_list(ctx, "");//"RC4-MD5"
	return true;
}

        初始化做了几件事:

  • 启动库SSL_library_init
  • 加载错误信息字符串SSL_load_error_strings,后面有错就能输出字符串信息
  • 初始化算法库OpenSSL_add_ssl_algorithms
  • 创建上下文对象同时指定协议版本SSL_CTX_new,代码中用的是SSLv23_server_method
  • 设置验证模式,单向还是双向,SSL_VERIFY_NONE就是单向,服务端不需要验证客户端(客户端验证服务端是必须的,否则就不是安全通信了),对web服务一般不用SSL_VERIFY_PEER,因为客户方会太麻烦,对于公开服务也没必要
  • 设置证书文件和私钥文件并验证私钥和证书匹配
  • 设置允许的加密算法SSL_CTX_set_cipher_list,有默认值

        初始化过程和服务端socket操作是没有关系的,仍然需要原来的建立服务socket、监听端口、接受连接的过程,接受连接之后在连接之上通过发送复杂的交互数据来建立SSL连接。

1.3 建立SSL连接SSL_accept

SSL* CmySSLTLS::getSSL(int sd)
{
	int err;
	SSL* ssl = SSL_new(ctx);
	if (ssl == NULL) exit(1);
	SSL_set_fd(ssl, sd);
	err = SSL_accept(ssl);
	printf("SSL_accept finished %d\n", err);
	if ((err) == -1)
	{
		ERR_print_errors_fp(stderr);
		SSL_free(ssl);
		return NULL;
	}

	/*打印所有加密算法的信息(可选)*/
	printf("SSL connection using %s\n", SSL_get_cipher(ssl));

	/*得到服务端的证书并打印些信息(可选) */
	X509* client_cert;
	client_cert = SSL_get_peer_certificate(ssl);
	if (client_cert != NULL)
	{
		printf("Client certificate:\n");

		char* str = X509_NAME_oneline(X509_get_subject_name(client_cert), 0, 0);
		if (str == NULL) exit(1);
		printf("\t subject: %s\n", str);
		free(str);

		str = X509_NAME_oneline(X509_get_issuer_name(client_cert), 0, 0);
		if (str == NULL) exit(1);
		printf("\t issuer: %s\n", str);
		free(str);

		X509_free(client_cert);/*如不再需要,需将证书释放 */
	}
	else
		printf("Client does not have certificate.\n");
	return ssl;
}

        大部分代码都是输出调试信息,最关键只是用SSL_accept来实现握手过程(先通过SSL_set_fd来把SSL对象和原始的socket连接绑定在一起)。

        握手过程很复杂,但是对我们只是一个调用。

1.4 发送和接收SSL_write SSL_read

        此时再用原始的发送和接收函数只能得到加密后的数据,需要替换为openssl的发送接收函数:

#ifdef ENABLE_HTTPS //用ssl发送
				if (ssl)n = SSL_write(ssl, buf + i, count - i);
				else n = send(s, buf + i, count - i, 0);
#else
				n = send(s, buf + i, count - i, 0);
#endif

#ifdef ENABLE_HTTPS //用ssl接收
			if (ssl)*pReadCount = SSL_read(ssl, buf, buflen);
			else *pReadCount = recv(s, buf, buflen, 0);
#else
			*pReadCount = recv(s, buf, buflen, 0);
#endif

        这个代码位于function/mysocket.h,相关修改都由ENABLE_HTTPS宏控制。

        ssl是SSL*,如果不为空就执行SSL的发送和接收,否则就是原始的发送和接收,这样就兼顾了SSL和普通socket。

二、服务器的改造

        相关代码位于myhttpserver.h。使用了前面介绍的包装类。

2.1 服务器初始化

	public://ISocketServerProcess
#ifdef ENABLE_HTTPS //定义ssl的ctx
		CmySSLTLS ctx;
#endif
		//服务开始时调用(主进程)
		virtual bool OnStartServer()
		{
#ifdef ENABLE_HTTPS //初始化ssl的ctx
			if (!ctx.Init_SSL_CTX())
			{
				return false;
			}
#endif

        这个其实放哪里都行。

2.2 建立SSL连接

		//处理一个已经建立的连接
		virtual bool SocketProcess(bool _isMgrPort, CMySocket & _s, long * pRet, long i_child)
		{
            。。。。。。

			m_s=_s;
			。。。。。

#ifdef ENABLE_HTTPS //获得SSL
			m_s.ssl = this->ctx.getSSL(m_s.GetFD());
#endif

        这里传入的_s就是原始连接,还没有收发任何数据的。在这里执行握手并把SSL放在CMySocket对象里,后续收发就会自动调用SSL函数了。

2.3 释放

		//服务结束时调用(主进程)
		virtual bool OnStopServer()
		{
#ifdef ENABLE_HTTPS //释放ssl的ctx
			ctx.free_SSL_CTX();
#endif



		//处理一个已经建立的连接
		virtual bool SocketProcess(bool _isMgrPort, CMySocket & _s, long * pRet, long i_child)
		{
。。。。。。

#ifdef ENABLE_HTTPS //释放SSL
			this->ctx.freeSSL(m_s.ssl);
			m_s.ssl = NULL;
#endif
			return true;
		}

三、测试代码

3.1 测试代码

int CmySSLTLS::_test_SSLTLS()
{
	int err;
	int listen_sd;
	struct sockaddr_in sa_serv;
	struct sockaddr_in sa_cli;
	socklen_t client_len;
	char     buf[4096];

	if (!Init_SSL_CTX())return __LINE__;

	/*开始正常的TCP socket过程.................................*/
	thelog << "Begin TCP socket..." << endi;

	listen_sd = socket(AF_INET, SOCK_STREAM, 0);
	CHK_ERR(listen_sd, "socket");

	memset(&sa_serv, '\0', sizeof(sa_serv));
	sa_serv.sin_family = AF_INET;
	sa_serv.sin_addr.s_addr = INADDR_ANY;
	sa_serv.sin_port = htons(PORT);

	err = bind(listen_sd, (struct sockaddr*)&sa_serv,

		sizeof(sa_serv));

	CHK_ERR(err, "bind");

	/*接受TCP链接*/
	err = listen(listen_sd, 5);
	CHK_ERR(err, "listen");

	thelog << "访问方式: https://www.test.com:" << PORT << "/" << endi;
	while (true)
	{
		int sd;
		
		client_len = sizeof(sa_cli);
		sd = accept(listen_sd, (struct sockaddr*)(void*)&sa_cli, &client_len);
		CHK_ERR(sd, "accept");

		thelog << "Connection from " << inet_ntoa(sa_cli.sin_addr) << ", port " << sa_cli.sin_port << endi;

		SSL* ssl= getSSL(sd);
		if (NULL == ssl)continue;

		/* 数据交换开始,用SSL_write,SSL_read代替write,read */
		err = SSL_read(ssl, buf, sizeof(buf) - 1);
		if ((err) == -1) { ERR_print_errors_fp(stderr); continue; }
		buf[err] = '\0';
		printf("Got %d chars:\n%s\n", err, buf);

		char outbuf[10240];
		time_t t1 = time(NULL);
		sprintf(outbuf, "HTTP/1.0 200 ok\r\nContent-type: text/plain\r\n\r\n%s %d %s request:\r\n%s"
			, __FILE__, __LINE__, ctime(&t1), buf);
		err = SSL_write(ssl, outbuf, strlen(outbuf));
		if ((err) == -1) { ERR_print_errors_fp(stderr); continue; }

		shutdown(sd, 2);
		freeSSL(ssl);
	}

	/* 收尾工作*/
	close(listen_sd);
	free_SSL_CTX();

	return 0;
}

int CmySSLTLS::test_SSLTLS()
{
	CmySSLTLS me;
	return me._test_SSLTLS();
}

        测试代码是完全独立的(不是前面的完整服务器),只会将收到的全部信息作为文本返回。

3.2 配置、运行和结果

        测试代码入口在myhttpd_t.cpp的main函数里,被注释掉了。打开即可执行测试程序。

        rebuild.sh编译,run.sh运行:

        注意必须用域名访问才能被浏览器认为证书正确,因为证书是给这个域名的(将httpd目录下的ca.cer安装到受信任的根证书存储区)。通过修改hosts文件来解决DNS问题(参见修改hosts文件,修改安全属性,建立自己的DNS-CSDN博客)。

        看到了吧,大功告成,没有安全警告。(直接看httpd的正式服务也可以,不过相关代码分散了,不便于学习)


(这里是文档结束) 


http://www.kler.cn/a/576458.html

相关文章:

  • 13.数据结构(软考)
  • Redis优化秒杀
  • 我的第一个CVE漏洞挖掘之旅
  • 可视化+图解:轻松搞定链表
  • C# CompareTo Compare 方法使用详解
  • 78.StringBuilder简单示例 C#例子 WPF例子
  • LeetCode hot 100—二叉树的最大深度
  • ubuntu22.04安装P104-100一些经验(非教程)
  • Spring Boot集成Minio笔记
  • pytest结合allure
  • 音频3A测试--AGC(自动增益)和NS(降噪)测试
  • 数组扩展【ES6】
  • 青少年编程与数学 02-010 C++程序设计基础 30课题、操作符重载
  • Cursor + IDEA 双开极速交互
  • 1658. 将 x 减到 0 的最小操作数
  • 代码随想录第五十天| 图论理论基础
  • Java集合框架之Collections工具类
  • 人工智能之数学基础:线性代数中的行列式的介绍
  • Node.js 报错 ENOBUFS 处理方案
  • Manus AI:从爆火到争议,AI Agent的未来之路