✨
This commit is contained in:
commit
9c05da023f
93
.eslintrc.js
Normal file
93
.eslintrc.js
Normal 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
6
.gitignore
vendored
Normal 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
16
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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
122
README.md
Normal file
@ -0,0 +1,122 @@
|
||||
# EasyNode
|
||||
|
||||
> 一个简易的个人Linux服务器管理面板(基于Node.js)
|
||||
|
||||
## 功能
|
||||
|
||||
> 通过`websocker实时更新`服务器基本信息: **系统、公网IP、CPU、内存、硬盘、网卡**等
|
||||
|
||||

|
||||
|
||||
> 解决`SSH跨端同步`问题——**Web SSH**
|
||||
|
||||

|
||||
|
||||
## 安装指南
|
||||
|
||||
### 服务端安装
|
||||
|
||||
- 依赖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
93
client/.eslintrc.js
Normal 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
|
||||
}
|
||||
}
|
3
client/app/config/index.js
Normal file
3
client/app/config/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
httpPort: 22022
|
||||
}
|
3
client/app/main.js
Normal file
3
client/app/main.js
Normal file
@ -0,0 +1,3 @@
|
||||
const { httpServer } = require('./server')
|
||||
|
||||
httpServer()
|
21
client/app/server.js
Normal file
21
client/app/server.js
Normal 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
|
||||
}
|
67
client/app/socket/monitor.js
Normal file
67
client/app/socket/monitor.js
Normal 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
21
client/app/utils/index.js
Normal 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
|
||||
}
|
83
client/app/utils/os-data.js
Normal file
83
client/app/utils/os-data.js
Normal 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
3
client/bin/www
Normal file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('start time: ', new Date())
|
||||
require('../app/main.js')
|
9
client/easynode-client.service
Normal file
9
client/easynode-client.service
Normal 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
34
client/package.json
Normal 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"
|
||||
}
|
||||
}
|
86
easynode-client-install.sh
Normal file
86
easynode-client-install.sh
Normal 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
|
34
easynode-client-uninstall.sh
Normal file
34
easynode-client-uninstall.sh
Normal 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"
|
8
easynode-server-install.sh
Normal file
8
easynode-server-install.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$(id -u)" != "0" ] ; then
|
||||
echo "***********************需root权限***********************"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 编写中...
|
BIN
images/list.png
Normal file
BIN
images/list.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 955 KiB |
BIN
images/webssh.png
Normal file
BIN
images/webssh.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 701 KiB |
24
package.json
Normal file
24
package.json
Normal 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"
|
||||
}
|
30
server/app/config/index.js
Normal file
30
server/app/config/index.js
Normal 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 // 是否记录日志
|
||||
}
|
||||
}
|
0
server/app/config/pem/.gitkeep
Normal file
0
server/app/config/pem/.gitkeep
Normal file
6
server/app/config/storage/host-list.json
Normal file
6
server/app/config/storage/host-list.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"host": "localhost",
|
||||
"name": "local"
|
||||
}
|
||||
]
|
7
server/app/config/storage/key.json
Normal file
7
server/app/config/storage/key.json
Normal 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-----"
|
||||
}
|
11
server/app/config/storage/ssh-record.json
Normal file
11
server/app/config/storage/ssh-record.json
Normal file
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 22,
|
||||
"username": "ubuntu",
|
||||
"type": "privateKey",
|
||||
"password": "",
|
||||
"privateKey": "test",
|
||||
"command": ""
|
||||
}
|
||||
]
|
61
server/app/controller/host-info.js
Normal file
61
server/app/controller/host-info.js
Normal 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
|
||||
}
|
6
server/app/controller/os-info.js
Normal file
6
server/app/controller/os-info.js
Normal file
@ -0,0 +1,6 @@
|
||||
let getOsData = require('../utils/os-data')
|
||||
|
||||
module.exports = async ({ res }) => {
|
||||
let data = await getOsData()
|
||||
res.success({ data })
|
||||
}
|
50
server/app/controller/ssh-info.js
Normal file
50
server/app/controller/ssh-info.js
Normal 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
|
||||
}
|
39
server/app/controller/user.js
Normal file
39
server/app/controller/user.js
Normal 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
41
server/app/init.js
Normal 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
10
server/app/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { httpServer, httpsServer, clientHttpServer } = require('./server')
|
||||
const initLocal = require('./init')
|
||||
|
||||
initLocal()
|
||||
|
||||
httpServer()
|
||||
|
||||
httpsServer()
|
||||
|
||||
clientHttpServer()
|
12
server/app/middlewares/body.js
Normal file
12
server/app/middlewares/body.js
Normal 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
|
||||
}
|
||||
})
|
5
server/app/middlewares/compress.js
Normal file
5
server/app/middlewares/compress.js
Normal file
@ -0,0 +1,5 @@
|
||||
const compress = require('koa-compress')
|
||||
|
||||
const options = { threshold: 2048 }
|
||||
|
||||
module.exports = compress(options)
|
14
server/app/middlewares/cors.js
Normal file
14
server/app/middlewares/cors.js
Normal 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
|
3
server/app/middlewares/history.js
Normal file
3
server/app/middlewares/history.js
Normal file
@ -0,0 +1,3 @@
|
||||
const { historyApiFallback } = require('koa2-connect-history-api-fallback')
|
||||
|
||||
module.exports = historyApiFallback({ whiteList: ['/api'] })
|
22
server/app/middlewares/index.js
Normal file
22
server/app/middlewares/index.js
Normal 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
|
||||
]
|
24
server/app/middlewares/jwt.js
Normal file
24
server/app/middlewares/jwt.js
Normal 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
|
54
server/app/middlewares/log4.js
Normal file
54
server/app/middlewares/log4.js
Normal 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()
|
31
server/app/middlewares/response.js
Normal file
31
server/app/middlewares/response.js
Normal 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
|
9
server/app/middlewares/router.js
Normal file
9
server/app/middlewares/router.js
Normal file
@ -0,0 +1,9 @@
|
||||
const router = require('../router')
|
||||
|
||||
const useRoutes = router.routes()
|
||||
const useAllowedMethods = router.allowedMethods()
|
||||
|
||||
module.exports = {
|
||||
useRoutes,
|
||||
useAllowedMethods
|
||||
}
|
6
server/app/middlewares/static.js
Normal file
6
server/app/middlewares/static.js
Normal file
@ -0,0 +1,6 @@
|
||||
const koaStatic = require('koa-static')
|
||||
const { staticDir } = require('../config')
|
||||
|
||||
const useStatic = koaStatic(staticDir)
|
||||
|
||||
module.exports = useStatic
|
12
server/app/router/index.js
Normal file
12
server/app/router/index.js
Normal 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
|
74
server/app/router/routes.js
Normal file
74
server/app/router/routes.js
Normal 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
61
server/app/server.js
Normal 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
|
||||
}
|
73
server/app/socket/clients.js
Normal file
73
server/app/socket/clients.js
Normal 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]
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
66
server/app/socket/monitor.js
Normal file
66
server/app/socket/monitor.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
71
server/app/socket/terminal.js
Normal file
71
server/app/socket/terminal.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
BIN
server/app/static/assets/bg.4d05532a.jpg
Normal file
BIN
server/app/static/assets/bg.4d05532a.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
32
server/app/static/assets/index.4226ec12.css
Normal file
32
server/app/static/assets/index.4226ec12.css
Normal file
File diff suppressed because one or more lines are too long
49
server/app/static/assets/index.49bfeae7.js
Normal file
49
server/app/static/assets/index.49bfeae7.js
Normal file
File diff suppressed because one or more lines are too long
BIN
server/app/static/favicon.ico
Normal file
BIN
server/app/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
15
server/app/static/index.html
Normal file
15
server/app/static/index.html
Normal 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
120
server/app/utils/index.js
Normal 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}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]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
|
||||
}
|
84
server/app/utils/os-data.js
Normal file
84
server/app/utils/os-data.js
Normal 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
54
server/package.json
Normal 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"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user