1、功能要求:
使用web网页充当ssh客户端,达到在网页端输入linux命令可以正常返回ssh服务端结果的效果。
2、技术选择:
2.1 传输协议的选择
根据以上功能描述,如果用http协议则只能期待浏览器发送请求才能得到服务端的结果响应。但是有些linux命令的结果是持续输出,再考虑到网络开销,所以需要一个客户端与服务端能持续交互的工具,而websocket协议的特性正符合要求。
若网页采用http, 则websocket采用ws;网页采用https, 则websocket采用wss.
2.2 前端界面模拟与交互
解决了传输协议问题, 考虑到在网页需要呈现一个与ssh终端相同的样式,又要能捕捉键盘输入事件, 方便与websocket配合把数据发送给服务端。了解到xtem.js早已集成完成了这个使命。
2.3 服务端语言的选择
服务端接收websocket传输来的数据,与ssh交互。这里使用一个php轮子,用于websocket数据的接收与发送。
Ratchet源码仓库:
Ratchet 官网介绍:
3.如何实现
3.1 服务端实现
第一步:安装依赖 socketo.me/docs/install
composer require cboden/ratchet
第二步:编写监听websocket服务启动入口
server.php (可考虑使用Linux脚本等方式,让服务常驻后台进程)
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Servidorsocket;
require dirname(__DIR__) . '/vendor/autoload.php';
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Servidorsocket() //实际用于处理websocket数据的类
)
),
8090 //监听websocket协议传输数据的端口
);
$server->run(); //启动服务
?>
第三步:处理websocket传来的数据,以及实现与ssh的交互
<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Servidorsocket implements MessageComponentInterface
{
protected $clients;
protected $connection = array();
protected $shell = array();
protected $conectado = array();
//ssh终端实际展示数据的宽度和高度
const COLS = 80;
const ROWS = 24;
public function __construct()
{
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn)
{
// Store the new connection to send messages to later
$this->clients->attach($conn);
$this->connection[$conn->resourceId] = null;
$this->shell[$conn->resourceId] = null;
$this->conectado[$conn->resourceId] = null;
}
public function onMessage(ConnectionInterface $from, $msg)
{
$data = json_decode($msg, true);
switch (key($data)) {
case 'data': //前端发送的单个字符
fwrite($this->shell[$from->resourceId], $data['data']['data']);
usleep(800);
//这个循环事必要的,用于持续输出后端的数据
while ($line = fgets($this->shell[$from->resourceId])) {
$from->send(mb_convert_encoding($line, "UTF-8"));
}
break;
case 'auth': //连接ssh服务器,需要php安装ssh2.so扩展
if ($this->connectSSH($data['auth']['server'], $data['auth']['port'], $data['auth']['user'], $data['auth']['password'], $from)) {
$from->send(mb_convert_encoding("Connected....", "UTF-8"));
while ($line = fgets($this->shell[$from->resourceId])) {
$from->send(mb_convert_encoding($line, "UTF-8"));
}
} else {
$from->send(mb_convert_encoding("Error, can not connect to the server. Check the credentials", "UTF-8"));
$from->close();
}
break;
default:
//例如:如果嵌套在管理端,可以用于定时检测用户登录态(具体细节待完善)
if ($this->conectado[$from->resourceId]) {
while ($line = fgets($this->shell[$from->resourceId])) {
$from->send(mb_convert_encoding($line, "UTF-8"));
}
}
break;
}
}
public function connectSSH($server, $port, $user, $password, $from)
{
$this->connection[$from->resourceId] = ssh2_connect($server, $port);
if (ssh2_auth_password($this->connection[$from->resourceId], $user, $password)) {
//$conn->send("Authentication Successful!\n");
$this->shell[$from->resourceId] = ssh2_shell($this->connection[$from->resourceId], 'xterm', null, self::COLS, self::ROWS, SSH2_TERM_UNIT_CHARS);
sleep(1); //这个时长相对合适
$this->conectado[$from->resourceId] = true;
return true;
} else {
return false;
}
}
public function onClose(ConnectionInterface $conn)
{
// The connection is closed, remove it, as we can no longer send it messages
$this->conectado[$conn->resourceId] = false;
$this->clients->detach($conn);
// Gracefully closes terminal, if it exists
if (isset($this->shell[$conn->resourceId]) && is_resource($this->shell[$conn->resourceId])) {
fclose($this->shell[$conn->resourceId]);
$this->shell[$conn->resourceId] = null;
}
}
public function onError(ConnectionInterface $conn, \Exception $e)
{
$conn->close();
}
}
第四步:nginx代理转发(不使用代理转发请求,直接访问websocket监听的端口也行,这里这样做是wss协议时,可以使用服务器上的SSL证书)
http{
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream webconsole {
server 127.0.0.1:websocket服务监听端口;
}
server {
listen 网页端口;
location /webconsole {
proxy_pass http://webconsole; //核心语句
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1; //核心语句
proxy_set_header Upgrade $http_upgrade; //核心语句
proxy_set_header Connection "upgrade"; //核心语句
}
}
}
3.2 网页端实现
index.html
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
<script src="node_modules/xterm/dist/xterm.js"></script>
<script src="node_modules/xterm/dist/addons/attach/attach.js"></script>
<script src="node_modules/xterm/dist/addons/fit/fit.js"></script>
<style>body {font-family: Arial, Helvetica, sans-serif;}
input[type=text], input[type=password], input[type=number] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
button:hover {
opacity: 0.8;
}
.serverbox {
padding: 16px;
border: 3px solid #f1f1f1;
width: 25%;
position: absolute;
top: 15%;
left: 37%;
}
</style>
</head>
<body>
<div id="serverbox" class="serverbox">
<label for="psw"><b>Server</b></label><br>
<input type="text" id="server" name="server" title="server" placeholder="server" /><br>
<label for="psw"><b>Port</b></label><br>
<input type="number" min="1" id="port" name="port" title="port" placeholder="port" /><br>
<label for="psw"><b>User</b></label><br>
<input type="text" id="user" name="user" title="user" placeholder="user" /><br>
<label for="psw"><b>Password</b></label><br>
<input type="password" id="password" name="password" title="password" placeholder="password" /><br>
<button type="button" onclick="ConnectServer()">Connect</button><br>
</div>
<div id="terminal" style="width:100%; height:90vh;visibility:hidden"></div>
<script>var resizeInterval;
var wSocket = new WebSocket("ws:127.0.0.1:8080");
Terminal.applyAddon(attach); // Apply the `attach` addon
Terminal.applyAddon(fit); //Apply the `fit` addon
var term = new Terminal({
cols: 80,
rows: 24
});
term.open(document.getElementById('terminal'));
function ConnectServer(){
document.getElementById("serverbox").style.visibility="hidden";
document.getElementById("terminal").style.visibility="visible";
var dataSend = {"auth":
{
"server":document.getElementById("server").value,
"port":document.getElementById("port").value,
"user":document.getElementById("user").value,
"password":document.getElementById("password").value
}
};
wSocket.send(JSON.stringify(dataSend));
term.fit();
term.focus();
}
wSocket.onopen = function (event) {
console.log("Socket Open");
term.attach(wSocket,false,false);
window.setInterval(function(){
wSocket.send(JSON.stringify({"refresh":""}));
}, 700);
};
wSocket.onerror = function (event){
term.detach(wSocket);
alert("Connection Closed");
}
term.on('data', function (data) {
var dataSend = {"data":{"data":data}};
wSocket.send(JSON.stringify(dataSend));
//Xtermjs with attach dont print zero, so i force. Need to fix it :(
if (data=="0"){
term.write(data);
}
})
//Execute resize with a timeout
window.onresize = function() {
clearTimeout(resizeInterval);
resizeInterval = setTimeout(resize, 400);
}
// Recalculates the terminal Columns / Rows and sends new size to SSH server + xtermjs
function resize() {
if (term) {
term.fit()
}
}
</script>
</body>
</html>
重要声明, 以上示例代码摘录于以下仓库:
github.com/roke22/PHP-SSH2-Web-Cli...
以上代码是功能实现的完整代码,而非部分示例。 均是亲身实践过可行的方案, 细节之处根据各项目的差异自行细微调整即可。