This commit is contained in:
zhulj 2022-06-08 16:47:41 +08:00
commit 9c05da023f
57 changed files with 4909 additions and 0 deletions

93
.eslintrc.js Normal file
View File

@ -0,0 +1,93 @@
// 规则参见https://cn.eslint.org/docs/rules/
module.exports = {
root: true, // 当前配置文件不能往父级查找
env: {
node: true,
es6: true
},
extends: [
'eslint:recommended' // 应用Eslint全部默认规则
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module' // 目标类型 Node项目得添加这个
},
// 自定义规则,可以覆盖 extends 的配置【安装Eslint插件可以静态检查本地文件是否符合以下规则】
'ignorePatterns': ['*.html', 'node-os-utils'],
rules: {
// 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许)
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'template-curly-spacing': ['error', 'always'], // 模板字符串空格
'default-case': 0,
'object-curly-spacing': ['error', 'always'],
'no-multi-spaces': ['error'],
indent: ['error', 2, { 'SwitchCase': 1 }], // 缩进2
quotes: ['error', 'single'], // 引号single单引 double双引
semi: ['error', 'never'], // 结尾分号never禁止 always必须
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
'no-multi-assign': 0,
'no-restricted-globals': 0,
'space-before-function-paren': 0, // 函数定义时括号前面空格
'one-var': 0, // 允许连续声明
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换
'no-constant-condition': 0, // if语句中禁止常量表达式
'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如xxx.hasOwnProperty)
'no-regex-spaces': 0, // 允许正则匹配多个空格
'no-unexpected-multiline': 0, // 允许多行表达式
'no-fallthrough': 0, // 允许switch穿透
'no-delete-var': 0, // 允许 delete 删除对象属性
'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用
'no-class-assign': 0, // 允许修改class类型
'no-param-reassign': 0, // 允许对函数params赋值
'max-len': 0, // 允许长行
'func-names': 0, // 允许命名函数
'import/no-unresolved': 0, // 不检测模块not fund
'import/prefer-default-export': 0, // 允许单个导出
'no-const-assign': 1, // 警告修改const命名的变量
'no-unused-vars': 1, // 警告:已声明未使用
'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符
'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN
'no-var': 2, // 禁止使用var声明
'no-empty-pattern': 2, // 空解构赋值
'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !==
'no-cond-assign': 2, // if语句中禁止赋值
'no-dupe-args': 2, // 禁止function重复参数
'no-dupe-keys': 2, // 禁止object重复key
'no-duplicate-case': 2,
'no-empty': 2, // 禁止空语句块
'no-func-assign': 2, // 禁止重复声明函数
'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明
'no-sparse-arrays': 2, // 禁止稀缺数组
'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码
'no-unsafe-finally': 2, // 禁止finally出现控制流语句return、throw等因为这会导致try...catch捕获不到
'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
// auto format options
'prefer-const': 0, // 禁用声明自动化
'no-extra-parens': 0, // 允许函数周围出现不明括号
'no-extra-semi': 2, // 禁止不必要的分号
// curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号
'dot-notation': 0, // 允许使用点号或方括号来访问对象属性
'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行
'no-else-return': 2, // 禁止if中有return后又else
'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换allow字段内符号允许
'no-trailing-spaces': 1, //一行结束后面不要有空格
'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行
'no-useless-return': 2,
'wrap-iife': 0, // 允许自调用函数
'yoda': 0, // 允许yoda语句
'strict': 0, // 允许strict
'no-undef-init': 0, // 允许将变量初始化为undefined
'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因
'consistent-return': 0, // 允许函数不使用return
'no-new': 0, // 允许单独new
'no-restricted-syntax': 0, // 允许特定的语法
'no-plusplus': 0,
'import/extensions': 0, // 忽略扩展名
'global-require': 0,
'no-return-assign': 0
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
!.gitkeep
dist
server/app/static/upload/*
server/app/socket/temp/*
server/app/logs/*

16
CHANGELOG.md Normal file
View File

@ -0,0 +1,16 @@
## [1.0.0](https://github.com/chaos-zhu/easynode/releases) (2022-06-08)
### Features
* 通过`websocker实时更新`服务器基本信息: 系统、公网IP、CPU、内存、硬盘、网卡等
* 解决`SSH跨端同步`问题——Web SSH
<!-- ### Bug Fixes
* 修复排序后重连socket的bug
### Trivial Changes
* 优化背景色 ([36ca9e5](https://github.com/nodejs/changelog-maker/commit/36ca9e50c5b52594713b4cd5e4c75e964e1e7c7b)) -->

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Sunil Wang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
README.md Normal file
View File

@ -0,0 +1,122 @@
# EasyNode
> 一个简易的个人Linux服务器管理面板(基于Node.js)
## 功能
> 通过`websocker实时更新`服务器基本信息: **系统、公网IP、CPU、内存、硬盘、网卡**等
![服务器列表](./images/list.png)
> 解决`SSH跨端同步`问题——**Web SSH**
![webssh功能](./images/webssh.png)
## 安装指南
### 服务端安装
- 依赖Node.js环境
- 建议使用**境外服务器(最好延迟低)**安装服务端客户端信息监控与webssh功能都将以`该服务器作为跳板机`
- 占用端口8082(http端口)、8083(https端口)、22022(客户端端口)
#### Docker镜像
> 注意网速统计功能可能受限预计v2.0版本修复
> https服务需自行配置证书然后构建镜像或者使用`nginx反代`解决(推荐)
- docker run -d -p 8082:8082 -p 22022:22022 easynode
#### 一键脚本
> 编写中...
<!-- ```shell
# 国外环境
wget -N --no-check-certificate xxx && bash easynode-server-install.sh
# 国内环境
wget -N --no-check-certificate xxx && bash easynode-server-install.sh
``` -->
#### 手动部署
1. 安装Node.js
2. 拉取代码:
3. 安装依赖:
4. 配置域名:
5. 启动服务:
6. 访问http://domain:8082
- 默认登录密码admin(首次部署完成后请及时修改).
6. 部署https服务
- 部署https服务需要自己上传域名证书至`\server\app\config\pem`,并且证书和私钥分别命名:`key.pem``cert.pem`
- 不出意外你就可以访问https服务https://domain:8083
---
### 客户端安装
> 占用端口22022
> 支持后续一键升级、支持重复安装
```shell
# 国外环境
wget -N --no-check-certificate xxx && bash easynode-client-install.sh
# 国内环境
wget -N --no-check-certificate xxx && bash easynode-client-install.sh
```
> 卸载:无任何文件或服务残留
```shell
# 国外环境
wget -N --no-check-certificate xxx && bash easynode-client-uninstall.sh
# 国内环境
wget -N --no-check-certificate xxx && bash easynode-client-uninstall.sh
```
> 查看客户端日志
```shell
journalctl --follow -u easynode-client
```
---
## 安全与说明
> 本人非专业后端,此服务全凭兴趣开发. 由于知识受限,并不能保证没有漏洞的存在,生产服务器请慎重使用此服务.
> 所有服务器信息相关接口已做`jwt鉴权`
> webssh功能需要的密钥信息全部保存在服务端服务器的`app\config\storage\ssh-record.json`中. 在保存ssh密钥信息到服务器时v1.0版本未做加密,`如果使用此功能最好带上https`, 并且保管好你的服务端服务器密码.
## 技术架构
> 待更新...
## v2.0功能方向
- 终端快捷键
- 终端常用指令
- 终端多tab支持
- 终端主题
- FTP文件系统
- 支持完整功能的docker镜像
## License
[MIT](LICENSE). Copyright (c).

93
client/.eslintrc.js Normal file
View File

@ -0,0 +1,93 @@
// 规则参见https://cn.eslint.org/docs/rules/
module.exports = {
root: true, // 当前配置文件不能往父级查找
env: {
node: true,
es6: true
},
extends: [
'eslint:recommended' // 应用Eslint全部默认规则
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module' // 目标类型 Node项目得添加这个
},
// 自定义规则,可以覆盖 extends 的配置【安装Eslint插件可以静态检查本地文件是否符合以下规则】
'ignorePatterns': ['*.html'],
rules: {
// 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许)
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'template-curly-spacing': ['error', 'always'], // 模板字符串空格
'default-case': 0,
'object-curly-spacing': ['error', 'always'],
'no-multi-spaces': ['error'],
indent: ['error', 2, { 'SwitchCase': 1 }], // 缩进2
quotes: ['error', 'single'], // 引号single单引 double双引
semi: ['error', 'never'], // 结尾分号never禁止 always必须
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
'no-multi-assign': 0,
'no-restricted-globals': 0,
'space-before-function-paren': 0, // 函数定义时括号前面空格
'one-var': 0, // 允许连续声明
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换
'no-constant-condition': 0, // if语句中禁止常量表达式
'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如xxx.hasOwnProperty)
'no-regex-spaces': 0, // 允许正则匹配多个空格
'no-unexpected-multiline': 0, // 允许多行表达式
'no-fallthrough': 0, // 允许switch穿透
'no-delete-var': 0, // 允许 delete 删除对象属性
'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用
'no-class-assign': 0, // 允许修改class类型
'no-param-reassign': 0, // 允许对函数params赋值
'max-len': 0, // 允许长行
'func-names': 0, // 允许命名函数
'import/no-unresolved': 0, // 不检测模块not fund
'import/prefer-default-export': 0, // 允许单个导出
'no-const-assign': 1, // 警告修改const命名的变量
'no-unused-vars': 1, // 警告:已声明未使用
'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符
'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN
'no-var': 2, // 禁止使用var声明
'no-empty-pattern': 2, // 空解构赋值
'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !==
'no-cond-assign': 2, // if语句中禁止赋值
'no-dupe-args': 2, // 禁止function重复参数
'no-dupe-keys': 2, // 禁止object重复key
'no-duplicate-case': 2,
'no-empty': 2, // 禁止空语句块
'no-func-assign': 2, // 禁止重复声明函数
'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明
'no-sparse-arrays': 2, // 禁止稀缺数组
'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码
'no-unsafe-finally': 2, // 禁止finally出现控制流语句return、throw等因为这会导致try...catch捕获不到
'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
// auto format options
'prefer-const': 0, // 禁用声明自动化
'no-extra-parens': 0, // 允许函数周围出现不明括号
'no-extra-semi': 2, // 禁止不必要的分号
// curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号
'dot-notation': 0, // 允许使用点号或方括号来访问对象属性
'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行
'no-else-return': 2, // 禁止if中有return后又else
'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换allow字段内符号允许
'no-trailing-spaces': 1, //一行结束后面不要有空格
'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行
'no-useless-return': 2,
'wrap-iife': 0, // 允许自调用函数
'yoda': 0, // 允许yoda语句
'strict': 0, // 允许strict
'no-undef-init': 0, // 允许将变量初始化为undefined
'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因
'consistent-return': 0, // 允许函数不使用return
'no-new': 0, // 允许单独new
'no-restricted-syntax': 0, // 允许特定的语法
'no-plusplus': 0,
'import/extensions': 0, // 忽略扩展名
'global-require': 0,
'no-return-assign': 0
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
httpPort: 22022
}

3
client/app/main.js Normal file
View File

@ -0,0 +1,3 @@
const { httpServer } = require('./server')
httpServer()

21
client/app/server.js Normal file
View File

@ -0,0 +1,21 @@
const http = require('http')
const Koa = require('koa')
const { httpPort } = require('./config')
const wsOsInfo = require('./socket/monitor')
const httpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
serverHandler(app, server)
server.listen(httpPort, () => {
console.log(`Server(http) is running on port:${ httpPort }`)
})
}
function serverHandler(app, server) {
wsOsInfo(server)
}
module.exports = {
httpServer
}

View File

@ -0,0 +1,67 @@
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('Task: start getIpInfo', new Date())
getIpInfo()
})
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
console.log('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}

21
client/app/utils/index.js Normal file
View File

@ -0,0 +1,21 @@
const axios = require('axios')
const getLocalNetIP = async () => {
try {
let ipUrls = ['http://ip-api.com/json/?lang=zh-CN', 'http://whois.pconline.com.cn/ipJson.jsp?json=true']
let { data } = await Promise.race(ipUrls.map(url => axios.get(url)))
return data.ip || data.query
} catch (error) {
console.error('getIpInfo Error: ', error)
return {
ip: '未知',
country: '未知',
city: '未知',
error
}
}
}
module.exports = {
getLocalNetIP
}

View File

@ -0,0 +1,83 @@
const osu = require('node-os-utils')
const os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(200)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut()
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
return err.toString()
}
}

3
client/bin/www Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
console.log('start time: ', new Date())
require('../app/main.js')

View File

@ -0,0 +1,9 @@
[Unit]
Description=easynode client server port_22022
[Service]
ExecStart=/root/local/easynode-client/easynode-client
WorkingDirectory=/root/local/easynode-client
[Install]
WantedBy=multi-user.target

34
client/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "easynode-client",
"version": "1.0.0",
"description": "easynode-client",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist"
},
"scripts": {
"client": "nodemon ./app/main.js",
"pkgwin": "pkg . -t node16-win-x64",
"pkglinux": "pkg . -t node16-linux-x64"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"axios": "^0.21.4",
"koa": "^2.13.1",
"node-os-utils": "^1.3.6",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1"
},
"devDependencies": {
"eslint": "^7.32.0",
"nodemon": "^2.0.15",
"pkg": "5.6"
}
}

View File

@ -0,0 +1,86 @@
#!/bin/bash
if [ "$(id -u)" != "0" ] ; then
echo "***********************需root权限***********************"
exit 1
fi
SERVER_NAME=easynode-client
FILE_PATH=/root/local/easynode-client
SERVICE_PATH=/etc/systemd/system
SERVER_VERSION=v1.0
echo "***********************开始安装 easynode-client_${SERVER_VERSION}***********************"
systemctl status ${SERVER_NAME} > /dev/null 2>&1
if [ $? != 4 ]
then
echo "***********************停用旧服务***********************"
systemctl stop ${SERVER_NAME}
systemctl disable ${SERVER_NAME}
systemctl daemon-reload
fi
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
then
echo "***********************移除旧服务***********************"
chmod 777 ${SERVICE_PATH}/${SERVER_NAME}.service
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
systemctl daemon-reload
fi
if [ -d ${FILE_PATH} ]
then
echo "***********************移除旧文件***********************"
chmod 777 ${FILE_PATH}
rm -Rf ${FILE_PATH}
fi
# 开始安装
echo "***********************创建文件PATH***********************"
mkdir -p ${FILE_PATH}
echo "***********************下载开始***********************"
DOWNLOAD_FILE_URL="http://221022.xyz:8000/s/MZBPmdw2w8RXrYK/download/easynode-client"
DOWNLOAD_SERVICE_URL="http://221022.xyz:8000/s/25nQLDECkW6PtL8/download/easynode-client.service"
# -O 指定路径和文件名(这里是二进制文件, 不需要扩展名)
wget -O ${FILE_PATH}/${SERVER_NAME} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
if [ $? != 0 ]
then
echo "***********************下载${SERVER_NAME}失败***********************"
exit 1
fi
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
if [ $? != 0 ]
then
echo "***********************下载${SERVER_NAME}.service失败***********************"
exit 1
fi
echo "***********************下载成功***********************"
echo "***********************设置权限***********************"
chmod +x ${FILE_PATH}/${SERVER_NAME}
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
echo "***********************移动service&reload***********************"
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
echo "***********************daemon-reload***********************"
systemctl daemon-reload
echo "***********************启动服务***********************"
systemctl start ${SERVER_NAME}
echo "***********************设置开机启动***********************"
systemctl enable ${SERVER_NAME}
echo "***********************完成安装并启动***********************"
echo "***********************删除脚本***********************"
rm $0

View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
if [ "$(id -u)" != "0" ] ; then
echo "***********************需root权限***********************"
exit 1
fi
SERVER_NAME=easynode-client
FILE_PATH=/root/local/easynode-client
SERVICE_PATH=/etc/systemd/system
echo "*********************** 开始卸载 ***************************"
service ${SERVER_NAME} stop
systemctl disable ${SERVER_NAME}
echo "*********************** 移除文件 ***************************"
if [ -d "${FILE_PATH}" ]
then
rm -Rf ${FILE_PATH}
fi
echo "*********************** 移除服务 ***************************"
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
then
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
systemctl daemon-reload
fi
echo "*********************** 卸载完成 ***************************"
echo "***********************删除脚本***********************"
rm "$0"

View File

@ -0,0 +1,8 @@
#!/bin/bash
if [ "$(id -u)" != "0" ] ; then
echo "***********************需root权限***********************"
exit 1
fi
# 编写中...

BIN
images/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

BIN
images/webssh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "easynode",
"version": "1.0.0",
"description": "easy to manage the server",
"private": true,
"workspaces": ["server", "client"],
"repository": {
"type": "git",
"url": "git+https://github.com/chaos-zhu/easynode.git"
},
"keywords": [
"vps",
"node",
"easynode",
"chaos",
"chaoszhu"
],
"author": "chaoszhu",
"license": "ISC",
"bugs": {
"url": "https://github.com/chaos-zhu/easynode/issues"
},
"homepage": "https://github.com/chaos-zhu/easynode#readme"
}

View File

@ -0,0 +1,30 @@
const path = require('path')
const fs = require('fs')
const getCertificate =() => {
try {
return {
cert: fs.readFileSync(path.join(__dirname, './pem/cert.pem')),
key: fs.readFileSync(path.join(__dirname, './pem/key.pem'))
}
} catch (error) {
return null
}
}
module.exports = {
domain: 'yourDomain', // 域名xxx.com
httpPort: 8082,
httpsPort: 8083,
clientPort: 22022, // 勿更改
certificate: getCertificate(),
uploadDir: path.join(process.cwd(),'./app/static/upload'),
staticDir: path.join(process.cwd(),'./app/static'),
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
keyPath: path.join(__dirname,'./storage/key.json'),
hostListPath: path.join(__dirname,'./storage/host-list.json'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),
flag: false // 是否记录日志
}
}

View File

View File

@ -0,0 +1,6 @@
[
{
"host": "localhost",
"name": "local"
}
]

View File

@ -0,0 +1,7 @@
{
"pwd": "admin",
"jwtExpires": "1h",
"jwtSecret": "E54CEp8AphsSthhyE36EYjzk4R2FWTJH",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCaozBBZnKSH0ZigZg+kQxG/lWV\np+lppeCGFwuLFTwc49eborW1zR9zlhIwXdrwjN3Si2ROesq69NMo3biIb9HrrJ9E\nKufuKXTxceKcCSjGs98Qa6bGZjziJzXMlICYcroPrMGPotLcpz0Zu6XMM+L0AaiS\nCu7sCkFlgY5o5xGi6wIDAQAB\n-----END PUBLIC KEY-----",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCaozBBZnKSH0ZigZg+kQxG/lWVp+lppeCGFwuLFTwc49eborW1\nzR9zlhIwXdrwjN3Si2ROesq69NMo3biIb9HrrJ9EKufuKXTxceKcCSjGs98Qa6bG\nZjziJzXMlICYcroPrMGPotLcpz0Zu6XMM+L0AaiSCu7sCkFlgY5o5xGi6wIDAQAB\nAoGAR7y6zyw6rGUL3vjl6uiZPHoStBmwY82LCkm4li4ks/ZS+KWUDKH7HEEbcQqp\nXfzLjzgRMYMvd2nKZ+PsDodpZ2YAoTutPI/YHou2jFhqR4Gt1HvibvGFVybfbrzV\nxvLVaQn4Rh2/SqTaDHaMgrHqmKRH0sUU42na3VKSm39YCAECQQDMrjSXEvOvKSta\nUjXF5T+6TctN33wzdk0B8vQ+Ca5ujGWcg6QeuAO2SU8cm5dSp6Ki7CENSsBsnB+6\n8i1IwvwBAkEAwWjg7UaoZ7caY2d6CKMOKXEnZTU5x3/sQD41dGkw5IHEPRxQbH27\nMP2dgCvrSJIVYqw/tUFp8ccyNkNU40xO6wJACHXm/Juu8P2dqiUdzelVAgl3Luff\nBW2Nb1gwmmPiDvXEuXyDizWGCcHsAD26OpNtWJi0IQ2G+LZXZW3fS1OsAQJBAJob\n832dG3roz0H9paNUKWikHPvr2Uo5iAn4h6dmWer561+2O+72kD2NF/6VADknDZs+\nHNVUdhKM4lmSdQVYPFkCQQC3qE0ChAbFeBwPquV+mzApezdWWKDdR+gL9UGrCWIL\nNbtRkv/HJHCqptbpUVaYPAT89Lt/TTlMP1eIYv/3t2ND\n-----END RSA PRIVATE KEY-----"
}

View File

@ -0,0 +1,11 @@
[
{
"host": "localhost",
"port": 22,
"username": "ubuntu",
"type": "privateKey",
"password": "",
"privateKey": "test",
"command": ""
}
]

View File

@ -0,0 +1,61 @@
const { readHostList, writeHostList } = require('../utils')
function getHostList({ res }) {
const data = readHostList()
res.success({ data })
}
function saveHost({ res, request }) {
let { body: { host: newHost, name } } = request
if(!newHost || !name) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
hostList.push({ host: newHost, name })
writeHostList(hostList)
res.success()
}
function updateHost({ res, request }) {
let { body: { host: newHost, name: newName, oldHost } } = request
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
writeHostList(hostList)
res.success()
}
function removeHost({ res, request }) {
let { body: { host } } = request
let hostList = readHostList()
let hostIdx = hostList.findIndex(item => item.host === host)
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
hostList.splice(hostIdx, 1)
writeHostList(hostList)
res.success({ data: `${ host }已移除` })
}
function updateHostSort({ res, request }) {
let { body: { list } } = request
if(!list) return res.fail({ msg: '参数错误' })
let hostList = readHostList()
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
let sortResult = []
for (let i = 0; i < list.length; i++) {
const curHost = list[i]
let temp = hostList.find(({ host }) => curHost.host === host)
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
sortResult.push(temp)
}
writeHostList(sortResult)
res.success({ msg: 'success' })
}
module.exports = {
getHostList,
saveHost,
updateHost,
removeHost,
updateHostSort
}

View File

@ -0,0 +1,6 @@
let getOsData = require('../utils/os-data')
module.exports = async ({ res }) => {
let data = await getOsData()
res.success({ data })
}

View File

@ -0,0 +1,50 @@
const { readSSHRecord, writeSSHRecord } = require('../utils')
const updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, command } } = request
let record = { host, port, username, type, password, privateKey, command }
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1)
sshRecord.push(record)
else
sshRecord.splice(idx, 1, record)
writeSSHRecord(sshRecord)
res.success({ data: '保存成功' })
}
const removeSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1)
writeSSHRecord(sshRecord)
res.success({ data: '移除成功' })
}
const existSSH = async ({ res, request }) => {
let { body: { host } } = request
let sshRecord = readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.success({ data: false })
res.success({ data: true })
}
const getCommand = async ({ res, request }) => {
let { host } = request.query
if(!host) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = readSSHRecord()
let record = sshRecord.find(item => item.host === host)
if(!record) return res.fail({ data: false, msg: 'host not found' })
const { command } = record
if(!command) return res.success({ data: false })
res.success({ data: command })
}
module.exports = {
updateSSH,
removeSSH,
existSSH,
getCommand
}

View File

@ -0,0 +1,39 @@
const jwt = require('jsonwebtoken')
const { readKey, writeKey, decrypt } = require('../utils')
const getpublicKey = ({ res }) => {
let { publicKey: data } = readKey()
if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
res.success({ data })
}
const login = async ({ res, request }) => {
let { body: { ciphertext } } = request
if(!ciphertext) return res.fail({ msg: '参数错误' })
try {
const password = decrypt(ciphertext)
let { pwd, jwtSecret, jwtExpires } = readKey()
if(password !== pwd) return res.fail({ msg: '密码错误' })
const token = jwt.sign({ date: Date.now() }, jwtSecret, { expiresIn: jwtExpires }) // 生成token
res.success({ data: { token, jwtExpires } })
} catch (error) {
res.fail({ msg: '解密失败' })
}
}
const updatePwd = async ({ res, request }) => {
let { body: { oldPwd, newPwd } } = request
oldPwd = decrypt(oldPwd)
newPwd = decrypt(newPwd)
let keyObj = readKey()
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
keyObj.pwd = newPwd
writeKey(keyObj)
res.success({ data: true, msg: 'success' })
}
module.exports = {
login,
getpublicKey,
updatePwd
}

41
server/app/init.js Normal file
View File

@ -0,0 +1,41 @@
const { getLocalNetIP, readHostList, writeHostList, readKey, writeKey, randomStr, isProd } = require('./utils')
const NodeRSA = require('node-rsa')
async function initIp() {
if(!isProd()) return console.log('非生产环境不初始化保存本地IP')
const localNetIP = await getLocalNetIP()
let vpsList = readHostList()
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
writeHostList(vpsList)
console.log('首次启动储存本机IP: ', localNetIP)
}
async function initRsa() {
let keyObj = readKey()
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在')
let key = new NodeRSA({ b: 1024 })
key.setOptions({ encryptionScheme: 'pkcs1' })
let privateKey = key.exportKey('pkcs1-private-pem')
let publicKey = key.exportKey('pkcs8-public-pem')
keyObj.privateKey = privateKey
keyObj.publicKey = publicKey
writeKey(keyObj)
console.log('新的公私钥已生成')
}
function randomJWTSecret() {
let keyObj = readKey()
if(keyObj.jwtSecret) return console.log('jwt secret已存在')
keyObj.jwtSecret = randomStr(32)
writeKey(keyObj)
console.log('已生成随机jwt secret')
}
module.exports = () => {
initIp()
initRsa()
randomJWTSecret()
}

10
server/app/main.js Normal file
View File

@ -0,0 +1,10 @@
const { httpServer, httpsServer, clientHttpServer } = require('./server')
const initLocal = require('./init')
initLocal()
httpServer()
httpsServer()
clientHttpServer()

View File

@ -0,0 +1,12 @@
const koaBody = require('koa-body')
const { uploadDir } = require('../config')
module.exports = koaBody({
multipart: true,
formidable: {
uploadDir,
keepExtensions: true,
multipart: true,
maxFieldsSize: 2 * 1024 * 1024
}
})

View File

@ -0,0 +1,5 @@
const compress = require('koa-compress')
const options = { threshold: 2048 }
module.exports = compress(options)

View File

@ -0,0 +1,14 @@
const cors = require('@koa/cors')
// const { domain } = require('../config')
const useCors = cors({
origin: ({ req }) => {
// console.log(req.headers.origin)
// return domain || req.headers.origin
return req.headers.origin
},
credentials: true,
allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]
})
module.exports = useCors

View File

@ -0,0 +1,3 @@
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
module.exports = historyApiFallback({ whiteList: ['/api'] })

View File

@ -0,0 +1,22 @@
const responseHandler = require('./response')
const useJwt = require('./jwt')
const useCors = require('./cors')
const useLog = require('./log4')
const useKoaBody = require('./body')
const { useRoutes, useAllowedMethods } = require('./router')
const useStatic = require('./static')
const compress = require('./compress')
const history = require('./history')
module.exports = [
compress,
history,
useStatic,
useCors,
responseHandler,
useKoaBody,
useLog,
useJwt,
useAllowedMethods,
useRoutes
]

View File

@ -0,0 +1,24 @@
const { verifyToken } = require('../utils')
const { apiPrefix } = require('../config')
let whitePath = [
'/login',
'/get-pub-pem'
].map(item => (apiPrefix + item))
const useJwt = async ({ request, res }, next) => {
const { path, headers: { token } } = request
if(whitePath.includes(path)) return next()
if(!token) return res.fail({ msg: '未登录', status: 403 })
const { code, msg } = verifyToken(token)
switch(code) {
case 1:
return await next()
case -1:
return res.fail({ msg, status: 401 })
case -2:
return res.fail({ msg: '登录态错误, 请重新登录', status: 401, data: msg })
}
}
module.exports = useJwt

View File

@ -0,0 +1,54 @@
const log4js = require('log4js')
const { outDir, flag } = require('../config').logConfig
log4js.configure({
appenders: {
out: {
type: 'stdout',
layout: {
type: 'colored'
}
},
cheese: {
type: 'file',
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
filename: `${ outDir }/receive.log`
}
},
categories: {
default: {
appenders: [ 'out', 'cheese' ],
level: 'info'
}
}
})
const logger = log4js.getLogger()
const useLog = () => {
return async (ctx, next) => {
const { method, path, origin, query, body, headers, ip } = ctx.request
const data = {
method,
path,
origin,
query,
body,
ip,
headers
}
await next()
if (flag) {
const { status, params } = ctx
data.status = status
data.params = params
data.result = ctx.body || 'no content'
if (String(status).startsWith(4) || String(status).startsWith(5))
logger.error(JSON.stringify(data))
else
logger.info(JSON.stringify(data))
}
}
}
module.exports = useLog()

View File

@ -0,0 +1,31 @@
const responseHandler = async (ctx, next) => {
ctx.res.success = ({ status, data, msg = 'success' } = {}) => {
ctx.status = status || 200
ctx.body = {
status: ctx.status,
data,
msg
}
}
ctx.res.fail = ({ status, msg = 'fail', data = {} } = {}) => {
ctx.status = status || 400
ctx.body = {
status,
data,
msg
}
}
try {
await next()
} catch (err) {
console.log('中间件错误:', err)
if (err.status)
ctx.res.fail({ status: err.status, msg: err.message })
else
ctx.app.emit('error', err, ctx)
}
}
module.exports = responseHandler

View File

@ -0,0 +1,9 @@
const router = require('../router')
const useRoutes = router.routes()
const useAllowedMethods = router.allowedMethods()
module.exports = {
useRoutes,
useAllowedMethods
}

View File

@ -0,0 +1,6 @@
const koaStatic = require('koa-static')
const { staticDir } = require('../config')
const useStatic = koaStatic(staticDir)
module.exports = useStatic

View File

@ -0,0 +1,12 @@
const { apiPrefix } = require('../config')
const koaRouter = require('koa-router')
const router = new koaRouter({ prefix: apiPrefix })
const routeList = require('./routes')
routeList.forEach(item => {
const { method, path, controller } = item
router[method](path, controller)
})
module.exports = router

View File

@ -0,0 +1,74 @@
const osInfo = require('../controller/os-info')
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh-info')
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info')
const { login, getpublicKey, updatePwd } = require('../controller/user')
const routes = [
{
method: 'get',
path: '/os-info',
controller: osInfo
},
{
method: 'post',
path: '/update-ssh',
controller: updateSSH
},
{
method: 'post',
path: '/remove-ssh',
controller: removeSSH
},
{
method: 'post',
path: '/exist-ssh',
controller: existSSH
},
{
method: 'get',
path: '/command',
controller: getCommand
},
{
method: 'get',
path: '/host-list',
controller: getHostList
},
{
method: 'post',
path: '/host-save',
controller: saveHost
},
{
method: 'put',
path: '/host-save',
controller: updateHost
},
{
method: 'post',
path: '/host-remove',
controller: removeHost
},
{
method: 'put',
path: '/host-sort',
controller: updateHostSort
},
{
method: 'get',
path: '/get-pub-pem',
controller: getpublicKey
},
{
method: 'post',
path: '/login',
controller: login
},
{
method: 'put',
path: '/pwd',
controller: updatePwd
}
]
module.exports = routes

61
server/app/server.js Normal file
View File

@ -0,0 +1,61 @@
const Koa = require('koa')
const compose = require('koa-compose')
const http = require('http')
const https = require('https')
const { clientPort } = require('./config')
const { domain, httpPort, httpsPort, certificate } = require('./config')
const middlewares = require('./middlewares')
const wsMonitorOsInfo = require('./socket/monitor')
const wsTerminal = require('./socket/terminal')
const wsClientInfo = require('./socket/clients')
const { throwError } = require('./utils')
const httpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
serverHandler(app, server)
server.listen(httpPort, () => {
console.log(`Server(http) is running on: http://localhost:${ httpPort }`)
})
}
const httpsServer = () => {
if(!certificate) return console.log('未上传证书, 创建https服务失败')
const app = new Koa()
const server = https.createServer(certificate, app.callback())
serverHandler(app, server)
server.listen(httpsPort, (err) => {
if (err) return console.log('https server error: ', err)
console.log(`Server(https) is running: https://${ domain }:${ httpsPort }`)
})
}
const clientHttpServer = () => {
const app = new Koa()
const server = http.createServer(app.callback())
wsMonitorOsInfo(server)
server.listen(clientPort, () => {
console.log(`Client(http) is running on: http://localhost:${ clientPort }`)
})
}
function serverHandler(app, server) {
wsTerminal(server)
wsClientInfo(server)
app.context.throwError = throwError
app.use(compose(middlewares))
app.on('error', (err, ctx) => {
ctx.status = 500
ctx.body = {
status: ctx.status,
message: `Program Error${ err.message }`
}
})
}
module.exports = {
httpServer,
httpsServer,
clientHttpServer
}

View File

@ -0,0 +1,73 @@
const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client')
const { readHostList } = require('../utils')
const { clientPort } = require('../config')
const { verifyToken } = require('../utils')
let clientSockets = {}, clientsData = {}, timer = null
function getClientsInfo(socketId) {
let hostList = readHostList()
hostList
.map(({ host }) => {
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
path: '/client/os-info',
forceNew: true,
reconnectionDelay: 3000,
reconnectionAttempts: 1
})
clientSockets[socketId].push(clientSocket)
return {
host,
clientSocket
}
})
.map(({ host, clientSocket }) => {
clientSocket
.on('connect', () => {
clientSocket.on('client_data', (osData) => {
clientsData[host] = osData
})
clientSocket.on('client_error', (error) => {
clientsData[host] = error
})
})
.on('connect_error', () => {
clientsData[host] = null
})
.on('disconnect', () => {
clientsData[host] = null
})
})
}
module.exports = (httpServer) => {
const serverIo = new ServerIO(httpServer, {
path: '/clients',
cors: {
}
})
serverIo.on('connection', (socket) => {
socket.on('init_clients_data', ({ token }) => {
const { code } = verifyToken(token)
if(code !== 1) return socket.emit('token_verify_fail', 'token无效')
clientSockets[socket.id] = []
getClientsInfo(socket.id)
socket.emit('clients_data', clientsData)
timer = setInterval(() => {
socket.emit('clients_data', clientsData)
}, 1500)
socket.on('disconnect', () => {
if(timer) clearInterval(timer)
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
delete clientSockets[socket.id]
})
})
})
}

View File

@ -0,0 +1,66 @@
const { Server } = require('socket.io')
const schedule = require('node-schedule')
const axios = require('axios')
let getOsData = require('../utils/os-data')
let serverSockets = {}, ipInfo = {}, osData = {}
async function getIpInfo() {
try {
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
console.log('getIpInfo Success: ', new Date())
ipInfo = data
} catch (error) {
console.log('getIpInfo Error: ', new Date(), error)
}
}
function ipSchedule() {
let rule1 = new schedule.RecurrenceRule()
rule1.second = [0, 10, 20, 30, 40, 50]
schedule.scheduleJob(rule1, () => {
let { query, country, city } = ipInfo || {}
if(query && country && city) return
console.log('Task: start getIpInfo', new Date())
getIpInfo()
})
let rule2 = new schedule.RecurrenceRule()
rule2.hour = 2
rule2.minute = 0
rule2.second = 0
schedule.scheduleJob(rule2, () => {
console.log('Task: refresh ip info', new Date())
getIpInfo()
})
}
ipSchedule()
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/client/os-info',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
serverSockets[socket.id] = setInterval(async () => {
try {
osData = await getOsData()
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
} catch (error) {
console.error('客户端错误:', error)
socket && socket.emit('client_error', { error })
}
}, 1500)
socket.on('disconnect', () => {
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id]
socket.close && socket.close()
socket = null
})
})
}

View File

@ -0,0 +1,71 @@
const { Server } = require('socket.io')
const { Client: Client } = require('ssh2')
const { readSSHRecord, verifyToken } = require('../utils')
function createTerminal(socket, vps) {
vps.shell({ term: 'xterm-color', cols: 1000, rows: 30 }, (err, stream) => {
if (err) return socket.emit('output', err.toString())
stream
.on('data', (data) => {
socket.emit('output', data.toString())
})
.on('close', () => {
vps.end()
})
socket.on('input', key => {
if(vps._sock.writable === false) return console.log('终端连接已关闭')
stream.write(key)
})
socket.emit('connect_terminal')
})
}
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/terminal',
cors: {
origin: '*'
}
})
serverIo.on('connection', (socket) => {
let vps = new Client()
socket.on('create', ({ host: ip, token }) => {
const { code } = verifyToken(token)
if(code !== 1) return socket.emit('token_verify_fail')
const sshRecord = readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
const { type, host, port, username } = loginInfo
try {
vps
.on('ready', () => {
socket.emit('connect_success', `已连接到服务器:${ host }`)
createTerminal(socket, vps)
})
.on('error', (err) => {
socket.emit('connect_fail', err.message)
})
.connect({
type: 'privateKey',
host,
port,
username,
[type]: loginInfo[type]
})
} catch (err) {
socket.emit('create_fail', err.message)
}
})
socket.on('disconnect', () => {
vps.end()
vps.destroy()
vps = null
})
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,15 @@
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
<title>EasyNode</title>
<script src="//at.alicdn.com/t/font_3309550_eg9tjmfmiku.js"></script>
<script type="module" crossorigin src="/assets/index.49bfeae7.js"></script>
<link rel="stylesheet" href="/assets/index.4226ec12.css">
</head>
<body>
<div id="app" style="opacity: 0.9;"></div>
</body>
</html>

120
server/app/utils/index.js Normal file
View File

@ -0,0 +1,120 @@
const fs = require('fs')
const jwt = require('jsonwebtoken')
const axios = require('axios')
const NodeRSA = require('node-rsa')
const { sshRecordPath, hostListPath, keyPath } = require('../config')
const readSSHRecord = () => {
let list
try {
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
} catch (error) {
writeSSHRecord([])
}
return list || []
}
const writeSSHRecord = (record = []) => {
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
}
const readHostList = () => {
let list
try {
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
} catch (error) {
writeHostList([])
}
return list || []
}
const writeHostList = (record = []) => {
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
}
const readKey = () => {
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
return keyObj
}
const writeKey = (keyObj = {}) => {
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
}
const getLocalNetIP = async () => {
try {
let ipUrls = ['http://ip-api.com/json/?lang=zh-CN', 'http://whois.pconline.com.cn/ipJson.jsp?json=true']
let { data } = await Promise.race(ipUrls.map(url => axios.get(url)))
return data.ip || data.query
} catch (error) {
console.error('getIpInfo Error: ', error)
return {
ip: '未知',
country: '未知',
city: '未知',
error
}
}
}
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
const err = new Error(msg)
err.status = status
throw err
}
const isIP = (ip = '') => {
const isIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/
const isIPv6 = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\dafAF]1,4)1,6|:)|:((:[\dafAF]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|([\dafAF]1,4:)2((:[\dafAF]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|([\dafAF]1,4:)4((:[\dafAF]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\dafAF]1,4:)6:|([\dafAF]1,4:)6:/
return isIPv4.test(ip) || isIPv6.test(ip)
}
const randomStr = (e) =>{
e = e || 32
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
a = str.length,
res = ''
for (let i = 0; i < e; i++) res += str.charAt(Math.floor(Math.random() * a))
return res
}
const verifyToken = (token) =>{
const { jwtSecret } = readKey()
try {
const { exp } = jwt.verify(token, jwtSecret)
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' }
return { code: 1, msg: 'success' }
} catch (error) {
return { code: -2, msg: error }
}
}
const isProd = () => {
const EXEC_ENV = process.env.EXEC_ENV || 'production'
return EXEC_ENV === 'production'
}
const decrypt = (ciphertext) => {
let { privateKey } = readKey()
const rsakey = new NodeRSA(privateKey)
rsakey.setOptions({ encryptionScheme: 'pkcs1' })
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
return plaintext
}
module.exports = {
readSSHRecord,
writeSSHRecord,
readHostList,
writeHostList,
getLocalNetIP,
throwError,
isIP,
readKey,
writeKey,
randomStr,
verifyToken,
isProd,
decrypt
}

View File

@ -0,0 +1,84 @@
const osu = require('node-os-utils')
const os = require('os')
let cpu = osu.cpu
let mem = osu.mem
let drive = osu.drive
let netstat = osu.netstat
let osuOs = osu.os
let users = osu.users
async function cpuInfo() {
let cpuUsage = await cpu.usage(300)
let cpuCount = cpu.count()
let cpuModel = cpu.model()
return {
cpuUsage,
cpuCount,
cpuModel
}
}
async function memInfo() {
let memInfo = await mem.info()
return {
...memInfo
}
}
async function driveInfo() {
let driveInfo = {}
try {
driveInfo = await drive.info()
} catch {
// console.log(driveInfo)
}
return driveInfo
}
async function netstatInfo() {
let netstatInfo = await netstat.inOut(300)
return netstatInfo === 'not supported' ? {} : netstatInfo
}
async function osInfo() {
let type = os.type()
let platform = os.platform()
let release = os.release()
let uptime = osuOs.uptime()
let ip = osuOs.ip()
let hostname = osuOs.hostname()
let arch = osuOs.arch()
return {
type,
platform,
release,
ip,
hostname,
arch,
uptime
}
}
async function openedCount() {
let openedCount = await users.openedCount()
return openedCount === 'not supported' ? 0 : openedCount
}
module.exports = async () => {
let data = {}
try {
data = {
cpuInfo: await cpuInfo(),
memInfo: await memInfo(),
driveInfo: await driveInfo(),
netstatInfo: await netstatInfo(),
osInfo: await osInfo(),
openedCount: await openedCount()
}
return data
} catch(err){
console.error('获取系统信息出错:', err)
return err.toString()
}
}

54
server/package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "easynode-server",
"version": "0.0.1",
"description": "easynode-server",
"bin": "./bin/www",
"pkg": {
"outputPath": "dist",
"scripts": "./*",
"assets": "./*"
},
"scripts": {
"local": "cross-env EXEC_ENV=local nodemon ./app/main.js",
"server": "cross-env EXEC_ENV=production nodemon ./app/main.js",
"start": "pm2 start ./app/main.js",
"pkgwin": "pkg . -t node16-win-x64",
"pkglinux": "pkg . -t node16-linux-x64"
},
"keywords": [],
"author": "",
"license": "ISC",
"nodemonConfig": {
"ignore": [
"*.json"
]
},
"dependencies": {
"@koa/cors": "^3.1.0",
"axios": "^0.21.4",
"is-ip": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.0",
"koa-jwt": "^4.0.3",
"koa-router": "^10.0.0",
"koa-sslify": "^5.0.0",
"koa-static": "^5.0.0",
"koa2-connect-history-api-fallback": "^0.1.3",
"log4js": "^6.4.4",
"node-os-utils": "^1.3.6",
"node-rsa": "^1.1.1",
"node-schedule": "^2.1.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.5.1",
"ssh2": "^1.10.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"nodemon": "^2.0.15",
"pkg": "5.6"
}
}

3010
yarn.lock Normal file

File diff suppressed because it is too large Load Diff