libssh2+websocketpp做后端,vue xterm做前端的web端shell终端

之前发布了一篇文章,前端是一样的,只不过后端用的nodejs技术做的后台脚本,那个脚本太简单了,毕竟是解释性语言,就是节省开发时间,而且性能也不会太差。
这次则用C++方式实现了一版后台,用了boost做xml解析和互斥锁,websocketpp做websocket库,libssh2则是与sshd交互的库。程序采用多线程方式,有心跳监测,经过大量测试,运行相对很稳定的一段程序。
后台典型代码:

// global.h
#ifndef __GLOBAL_H__
#define __GLOBAL_H__

#include <iostream>
#include <list>
#include <vector>
#include <map>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <thread>
#include <signal.h>

using namespace std;

#endif

// SSH.h
#ifndef _SSH_H_20220720__
#define _SSH_H_20220720__
#include <iostream>
#include <map>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <libgen.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <termios.h>
#include <libssh2.h>

using namespace std;

class SSH
{
private:
    const char *host = NULL;
    unsigned short port = 0;
    const char *username = NULL;
    const char *password = NULL;

    int sock = -1;
    LIBSSH2_SESSION *session = NULL;
    LIBSSH2_CHANNEL *channel = NULL;

public:
    SSH(const char *host, const unsigned short port, const char *username, const char *password);
    ~SSH();
    bool Connect();
    void DissConnect();
    ssize_t Write(const char *buffer, int n);
    ssize_t Read(char *buffer, int n);
    int Resize(int width, int height);
    int Socket() {return sock;}
    bool ChannelEof() {return libssh2_channel_eof(channel) == 1;}
};

#endif
// SSH.cpp
#include "SSH.h"

SSH::SSH(const char *host, const unsigned short port, const char *username, const char *password)
{
    this->host = host;
    this->port = port;
    this->username = username;
    this->password = password;
}

SSH::~SSH()
{
    DissConnect();
}

void SSH::DissConnect()
{
    if (channel)
    {
        libssh2_channel_free(channel);
        channel = NULL;
    }
    if (session)
    {
        libssh2_session_disconnect(session, "Session Shutdown, Thank you for playing");
        libssh2_session_free(session);
        session = NULL;
    }
    if (sock != -1)
    {
        close(sock);
        sock = -1;
    }
    libssh2_exit();
}

bool SSH::Connect()
{
    if (libssh2_init(0) != 0)
    {
        fprintf(stderr, "libssh2 initialization failed\n");
        return false;
    }

    LIBSSH2_SESSION *session = NULL;
    LIBSSH2_CHANNEL *channel = NULL;

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    this->sock = sock;
    unsigned long hostaddr = inet_addr(this->host);
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(this->port);
    sin.sin_addr.s_addr = hostaddr;
    if (connect(sock, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)) != 0)
    {
        fprintf(stderr, "Failed to established connection!\n");
        DissConnect();
        return false;
    }

    int sockFlags = fcntl(sock, F_GETFL, 0);
    fcntl(sock, F_SETFL, sockFlags | O_NONBLOCK);

    /* Open a session */
    session = libssh2_session_init();
    this->session = session;
    if (libssh2_session_startup(session, sock) != 0)
    {
        fprintf(stderr, "Failed Start the SSH session\n");
        DissConnect();
        return false;
    }

    /* Authenticate via password */
    if (libssh2_userauth_password(session, username, password) != 0)
    {
        fprintf(stderr, "Failed to authenticate\n");
        DissConnect();
        return false;
    }

    /* Open a channel */
    channel = libssh2_channel_open_session(session);
    if (channel == NULL)
    {
        fprintf(stderr, "Failed to open a new channel\n");
        DissConnect();
        return false;
    }
    this->channel = channel;

    /* Request a PTY */
    if (libssh2_channel_request_pty(channel, "xterm") != 0)
    {
        fprintf(stderr, "Failed to request a pty\n");
        DissConnect();
        return false;
    }

    /* Request a shell */
    if (libssh2_channel_shell(channel) != 0)
    {
        fprintf(stderr, "Failed to open a shell\n");
        DissConnect();
        return false;
    }

    libssh2_channel_set_blocking(channel, false);

    return true;
}

ssize_t SSH::Write(const char *buffer, int n)
{
    return libssh2_channel_write(channel, buffer, n);
}

ssize_t SSH::Read(char *buffer, int n)
{
    return libssh2_channel_read(channel, buffer, n);
}

int SSH::Resize(int width, int height)
{
    return libssh2_channel_request_pty_size(channel, width, height);
}

// websocket.h
#ifndef __WEBSOCKET_H_20220722__
#define __WEBSOCKET_H_20220722__

#include "global.h"

void RunWebsocketServer(); // 启动并运行websocket服务端
void WebSocketCheckClientAlive(); // 主动给客户端发心跳,保持其在线状态,踢掉超时的客户端

void RunEPollSockt();
#endif

// websocket.cpp
#include "SSH.h"

#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <json.hpp>

using namespace nlohmann;

#define EVENT_NUM 5

typedef websocketpp::server<websocketpp::config::asio> WebsocketServer;
typedef WebsocketServer::message_ptr message_ptr;

static map<string, string> config;
static WebsocketServer server;

// 标志唯一的客户端(client_ip_port变量):固定的前缀+客户端IP+客户端PORT组成,如 [::ffff:1.119.166.13]:61662

// 每个客户端 -> 连接句柄
static map<string, websocketpp::connection_hdl> client_hdl_map;

// 客户端连接句柄,对应的是活跃时间戳
static map<string, time_t> client_time_map;

static map<string, SSH *> client_ssh_map;
static map<int, SSH *> sock_ssh_map;
static map<int, websocketpp::connection_hdl> sock_hdl_map;

boost::mutex map_mutex; // 互斥锁

int epfd = -1;

void AddSockToEpoll(int sock)
{
	struct epoll_event ev;
	ev.data.fd = sock;						   // 要监视的文件描述符,可以是任何打开的在/proc/pid/fd/目录下的fd
	ev.events = EPOLLIN;					   // 监听读状态同时设置LT模式
	epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev); // 注册epoll事件
}

void DelSockFromEpoll(int sock)
{
	struct epoll_event ev;
	ev.data.fd = sock;						   // 要监视的文件描述符,可以是任何打开的在/proc/pid/fd/目录下的fd
	ev.events = EPOLLIN;					   // 监听读状态同时设置LT模式
	epoll_ctl(epfd, EPOLL_CTL_DEL, sock, &ev); // 移除epoll事件
}

static WebsocketServer::connection_ptr GetConFromHdl(websocketpp::connection_hdl hdl)
{
	try
	{
		return server.get_con_from_hdl(hdl);
	}
	catch (websocketpp::exception const &e)
	{
		cout << "GetConFromHdl failed: " << e.what() << endl;
		return NULL;
	}
}

bool OnValidate(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return false;
	}
	string client_ip_port = con->get_remote_endpoint();

	string key = "Sec-WebSocket-Protocol";
	string token = con->get_request_header(key);
	std::cout << key << ": " << token << std::endl;
	if (token != "shell")
	{
		return false;
	}

	map_mutex.lock();
	SSH *ssh = new SSH(config["sshhost"].c_str(), atoi(config["sshport"].c_str()), config["sshusername"].c_str(), config["sshpassword"].c_str());
	if (!ssh->Connect())
	{
		delete ssh;
		return false;
	}
	client_ssh_map[client_ip_port] = ssh;
	client_hdl_map[client_ip_port] = hdl;
	client_time_map[client_ip_port] = time(NULL);
	sock_ssh_map[ssh->Socket()] = ssh;
	sock_hdl_map[ssh->Socket()] = hdl;
	AddSockToEpoll(ssh->Socket());

	con->append_header(key, token); // 以后回复数据都要加这个头部信息

	cout << client_ip_port << " write to memory ok!" << endl;
	map_mutex.unlock();

	return true;
}

void SendMsgToClient(websocketpp::connection_hdl hdl, const string msg)
{
	string client_ip_port = "Unknown";
	try
	{
		WebsocketServer::connection_ptr con = server.get_con_from_hdl(hdl);
		client_ip_port = con->get_remote_endpoint();
		server.send(hdl, msg, websocketpp::frame::opcode::text);
	}
	catch (websocketpp::exception const &e)
	{
		std::cout << "send msg to " << client_ip_port << " failed because: "
				  << "(" << e.what() << ")" << std::endl;
	}
}

void RunEPollSockt()
{
	char buffer[4096];
	struct epoll_event events[EVENT_NUM];
	epfd = epoll_create(EVENT_NUM);
	while (1)
	{
		int nfds = epoll_wait(epfd, events, 5, -1);
		for (int i = 0; i < nfds; i++)
		{
			int fd = events[i].data.fd;
			SSH *ssh = sock_ssh_map[fd];
			websocketpp::connection_hdl hdl = sock_hdl_map[fd];
			string str;
			while (1)
			{
				memset(buffer, 0, sizeof buffer);
				if (ssh->Read(buffer, sizeof buffer - 1) > 0)
				{
					str += buffer;
				}
				else
				{
					break;
				}
			}
			if (!str.empty())
			{
				SendMsgToClient(hdl, str);
			}
		}
	}
}

void OnOpen(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return;
	}
	string client_ip_port = con->get_remote_endpoint();
	cout << "client connected: " << client_ip_port << endl;
	server->send(hdl, config["copyright"] + "\r\n", websocketpp::frame::opcode::text);
}

map<string, websocketpp::connection_hdl>::iterator EraseMemory(const string &client_ip_port)
{
	map<string, websocketpp::connection_hdl>::iterator iter = client_hdl_map.begin();
	if (client_ip_port == "Unknown" || client_ip_port.empty())
	{
		cout << client_ip_port << ", Cann't erase memory!" << endl;
		return iter;
	}
	iter = client_hdl_map.find(client_ip_port);
	if (iter != client_hdl_map.end())
	{
		cout << client_ip_port << " closed, erase memory for it..." << endl;
		iter = client_hdl_map.erase(iter);
		client_time_map.erase(client_ip_port);

		SSH *ssh = client_ssh_map[client_ip_port];
		if (ssh)
		{
			DelSockFromEpoll(ssh->Socket());
			sock_ssh_map.erase(ssh->Socket());
			sock_hdl_map.erase(ssh->Socket());
			client_ssh_map.erase(client_ip_port);
			delete ssh;
		}

		cout << client_ip_port << " erase memory success!" << endl;
	}
	else
	{
		cout << client_ip_port << " no need to erase memory!" << endl;
	}
	return iter;
}

void OnClose(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return;
	}
	map_mutex.lock();
	string client_ip_port = con->get_remote_endpoint();
	EraseMemory(client_ip_port);
	map_mutex.unlock();
}

void OnMessage(WebsocketServer *server, websocketpp::connection_hdl hdl, message_ptr data)
{
	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return;
	}
	string str = data->get_payload();
	json obj = json::parse(str, nullptr, false);
	if (obj.is_discarded())
	{
		cout << "Not valid msg: " << str << endl;
	}
	else
	{
		map_mutex.lock();
		string client_ip_port = con->get_remote_endpoint();
		SSH *ssh = client_ssh_map[client_ip_port];
		if (obj["Op"] == "stdin")
		{
			string msg = obj["Data"];
			cout << "receive msg from " << client_ip_port << ": " << msg << endl;
			ssh->Write(msg.c_str(), msg.size());
		}
		else
		{
			int width = obj["Cols"];
			int height = obj["Rows"];
			printf("%s resize pty width = %d height = %d\n", client_ip_port.c_str(), width, height);
			ssh->Resize(width, height);
		}
		map_mutex.unlock();
	}
}

// 如果客户端发来ping,可以更新活跃时间戳,并回复pong
bool OnPing(WebsocketServer *server, websocketpp::connection_hdl hdl, std::string payload)
{

	server->get_alog().write(websocketpp::log::alevel::app, payload);

	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return false;
	}

	map_mutex.lock();
	string client_ip_port = con->get_remote_endpoint();
	cout << "recv ping from " << client_ip_port << ": " << payload << endl;

	if (client_time_map.find(client_ip_port) != client_time_map.end())
	{
		client_time_map[client_ip_port] = time(NULL); // 客户端ping时更新时间戳
		try
		{
			server->pong(hdl, "pong"); // 客户端ping时回复pong
		}
		catch (websocketpp::exception const &e)
		{
			cout << "pong failed: " << e.what() << endl;
		}
	}
	map_mutex.unlock();
	return true;
}

// 服务器会定时发送ping给客户端,客户端会自动回复pong,回复时更新活跃时间戳
bool OnPong(WebsocketServer *server, websocketpp::connection_hdl hdl, std::string payload)
{
	server->get_alog().write(websocketpp::log::alevel::app, payload);

	WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
	if (!con)
	{
		return false;
	}
	string client_ip_port = con->get_remote_endpoint();
	cout << "recv pong from " << client_ip_port << ": " << payload << endl;

	map_mutex.lock();
	if (client_time_map.find(client_ip_port) != client_time_map.end())
	{
		client_time_map[client_ip_port] = time(NULL); // 客户端回复pong时更新活跃时间戳
	}
	map_mutex.unlock();

	return true;
}

static void ReadConfigXml()
{
	config["port"] = "8880";
	using boost::property_tree::ptree;
	ptree pt;
	ptree root;
	string cfg_path = "config.xml";
	try
	{
		read_xml(cfg_path, pt);
		root = pt.get_child("root");
	}
	catch (std::exception &e)
	{
		std::cout << "Error: " << e.what() << endl;
		return;
	}
	std::string str = pt.get<string>("root.server.<xmlattr>.port");
	if (!str.empty())
	{
		config["port"] = str;
	}

	str = pt.get<string>("root.ssh.<xmlattr>.host");
	if (!str.empty())
	{
		config["sshhost"] = str;
	}
	str = pt.get<string>("root.ssh.<xmlattr>.port");
	if (!str.empty())
	{
		config["sshport"] = str;
	}
	str = pt.get<string>("root.ssh.<xmlattr>.username");
	if (!str.empty())
	{
		config["sshusername"] = str;
	}
	str = pt.get<string>("root.ssh.<xmlattr>.password");
	if (!str.empty())
	{
		config["sshpassword"] = str;
	}
	str = pt.get<string>("root.ssh.<xmlattr>.copyright");
	if (!str.empty())
	{
		config["copyright"] = str;
	}
}

void RunWebsocketServer()
{
	ReadConfigXml();

	using websocketpp::lib::bind;
	using websocketpp::lib::placeholders::_1;
	using websocketpp::lib::placeholders::_2;

	server.set_reuse_addr(true); // 设置套接字选项SO_REUSEADDR

	server.set_ping_handler(bind(&OnPing, &server, ::_1, ::_2));
	server.set_pong_handler(bind(&OnPong, &server, ::_1, ::_2));

	// Set logging settings
	// server.set_access_channels(websocketpp::log::alevel::all);
	server.clear_access_channels(websocketpp::log::alevel::all); // 不输出日志

	// Initialize ASIO
	server.init_asio();

	// Register our open handler
	server.set_open_handler(bind(&OnOpen, &server, ::_1));

	// Register our close handler
	server.set_close_handler(bind(&OnClose, &server, _1));

	// Register our message handler
	server.set_message_handler(bind(&OnMessage, &server, _1, _2));

	server.set_validate_handler(bind(&OnValidate, &server, ::_1));

	// Listen on port 2152
	unsigned short port = atoi(config["port"].c_str());
	try
	{
		server.listen(port);
		cout << "listen at port " << port << endl;

		// Start the server accept loop
		server.start_accept();

		// Start the ASIO io_service run loop
		server.run();
	}
	catch (websocketpp::exception const &e)
	{
		cout << "error: " << e.what() << endl;
		exit(-1);
	}
}

// 定时扫描所有客户端,断开超时未回复的客户端
void WebSocketCheckClientAlive()
{
	int timeout = 3;
	while (1)
	{
		sleep(timeout);
		map_mutex.lock();
		printf("client size = %d\n", client_hdl_map.size());
		for (map<string, websocketpp::connection_hdl>::iterator iter = client_hdl_map.begin(); iter != client_hdl_map.end();)
		{
			string client_ip_port = iter->first;
			websocketpp::connection_hdl hdl = iter->second;
			printf("check [%s] alive?\n", client_ip_port.c_str());

			WebsocketServer::connection_ptr pconn;
			try
			{
				pconn = server.get_con_from_hdl(hdl);
			}
			catch (websocketpp::exception const &e)
			{
				cout << "get_con_from_hdl failed: " << e.what() << endl;
				iter = EraseMemory(client_ip_port);
				continue;
			}
			iter++;

			time_t last_seconds = client_time_map[client_ip_port];
			time_t this_seconds = time(NULL);
			int escaped_seconds = this_seconds - last_seconds;
			int pass_seconds = this_seconds - last_seconds;
			if (pass_seconds > timeout * 2 + 2)
			{
				printf("%s last pong is %d sec ago, timeout! Close it now!\n", client_ip_port.c_str(), escaped_seconds);
				try
				{
					pconn->close(websocketpp::close::status::normal, "timeout");
				}
				catch (websocketpp::exception const &e)
				{
					cout << "close failed: " << e.what() << endl;
				}
			}
			else if (pass_seconds >= timeout)
			{
				printf("%s last pong is %d sec ago. Now ping it!\n", client_ip_port.c_str(), escaped_seconds);
				try
				{
					server.ping(hdl, "ping");
				}
				catch (websocketpp::exception const &e)
				{
					cout << "ping failed: " << e.what() << endl;
				}
			}
			else
			{
				printf("%s last pong is %d sec ago! No need to ping!\n", client_ip_port.c_str(), escaped_seconds);
			}
		}
		map_mutex.unlock();
	}
}

// main.cpp
/**
 * @file main.cpp
 * @author 丁华能
 * @brief 连接过来的websocket带token,根据token查session中的信息,根据信息关联客户端,订阅消息,收到数据后根据关联客户端进行发送。
 * @version 0.1
 * @date 2022-05-18
 * 
 * @copyright Copyright (c) 2022
 * 
 */

#include "websocket.h"

using namespace std;

void epoll_thread()
{
	RunEPollSockt();
}

void websocket_thread()
{
	RunWebsocketServer();
}

void init_daemon(void){
    pid_t pid;
    int i;
 
    pid = fork();
    if(pid < 0)
    {
        printf("Error Fork\n");
        exit(1);
    }
    else if(pid > 0)
    {
        exit(0);
    }
 
    setsid();
	
    // umask(0);

	//忽略SIGCHLD信号
    signal(SIGCHLD, SIG_IGN);
 
    for(i=0; i<3; close(i++));
	open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
}

void ChangeDIRToCurrentApp(const char *argv0)
{
    string path(argv0);
    path = path.substr(0, path.find_last_of('/'));
    chdir(path.c_str());
}

int main(int argc, char **argv)
{
    ChangeDIRToCurrentApp(argv[0]);
    
	if (argc == 1)
	{
		init_daemon();
	}

	thread subscribe(epoll_thread);

	thread websocket(websocket_thread);

	WebSocketCheckClientAlive();

	subscribe.join();

	websocket.join();

	return 0;
}

<!-- config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<root>
    <server port="92" />
    <ssh host="127.0.0.1" port="22" username="test" password="111111" copyright="版权所有&copy"/>
</root> 

Makefile脚本内容如下:
.PHONY : clean

GCC = g++
ROOTDIR = …
INCLUDE = -I$(ROOTDIR)/lib/inc
LIB =
libs = -lboost_system -lpthread -lssh2
runlibs = -Wl,-rpath=.

src = $(wildcard *.cpp)
obj = $(patsubst %.cpp,%.o, $(src))

bin = sshserver
$(bin) : $(obj)
$(GCC) $(runlibs) $^ -o $@ $(LIB) $(libs)

$(obj): %.o : %.cpp
$(GCC) -c -g $(INCLUDE) $< -o $@

clean :
rm -f $(obj) $(bin)

websocketpp的下载地址:https://github.com/zaphoyd/websocketpp
boost的下载地址:https://www.boost.org/
libssh2下载地址:https://github.com/libssh2/libssh2

注意看main.cpp中支持参数部分,运行带任意参数就是调试模式,否则守护进程运行。

前端代码参考文章:https://blog.csdn.net/canlynetsky/article/details/125928915

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值