✨ v1.2 release
This commit is contained in:
parent
65f305df28
commit
ca60f4a87c
96
server/.eslintrc.js
Normal file
96
server/.eslintrc.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 规则参见:https://cn.eslint.org/docs/rules/
|
||||||
|
module.exports = {
|
||||||
|
root: true, // 当前配置文件不能往父级查找
|
||||||
|
'globals': {
|
||||||
|
'consola': 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,
|
||||||
|
'no-empty': 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-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
|
||||||
|
}
|
||||||
|
}
|
7
server/.gitignore
vendored
Normal file
7
server/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
app/static/upload/*
|
||||||
|
app/socket/temp/*
|
||||||
|
app/socket/.sftp-cache/*
|
||||||
|
app/logs/*
|
||||||
|
!.gitkeep
|
||||||
|
dist
|
40
server/README.md
Normal file
40
server/README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 面板服务端
|
||||||
|
|
||||||
|
- 基于Koa
|
||||||
|
|
||||||
|
## docker
|
||||||
|
|
||||||
|
<!-- 修改版本号 -->
|
||||||
|
- 构建镜像:docker build -t chaoszhu/easynode:v1.1 .
|
||||||
|
- 推送镜像:docker push chaoszhu/easynode:v1.1
|
||||||
|
|
||||||
|
> `docker run -d --net=host easynode-server`
|
||||||
|
<!-- > `docker run -d -p 8888:8082 -p 22022:22022 easynode-server` -->
|
||||||
|
|
||||||
|
## 遇到的问题
|
||||||
|
|
||||||
|
> MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 input listeners added to [Socket]. Use emitter.setMaxListeners() to increase limit
|
||||||
|
- ssh连接数过多(默认最多11个)
|
||||||
|
- 每次连接新建一个vps实例,断开则销毁
|
||||||
|
|
||||||
|
> Error signing data with key: error:0D07209B:asn1 encoding routines:ASN1_get_object:too long
|
||||||
|
- 经比对,ssh的rsa密钥在前端往后端的存储过程中丢失了部分字符
|
||||||
|
|
||||||
|
> 获取客户端信息:跨域请求客户端系统信息,建立ws socket实时更新网络
|
||||||
|
- 问题:服务端前端套上https后,前端无法请求客户端(http)的信息, 也无法建立ws socket连接(原因是https下无法建立http/ws协议请求)
|
||||||
|
- 方案1: 所有客户端与服务端通信,再全部由服务端与前端通信(考虑:服务端/客户端性能问题). Node实现http+https||nginx转发实现https
|
||||||
|
- 方案2: 给所有客户端加上https(客户端只有ip,没法给个人ip签订证书)
|
||||||
|
|
||||||
|
## 构建运行包
|
||||||
|
|
||||||
|
### 坑
|
||||||
|
|
||||||
|
> log4js: 该module使用到了fs.mkdir()等读写api,pkg打包后的环境不支持,设置保存日志的目录需使用process.cwd()】
|
||||||
|
|
||||||
|
> win闪退: 在linux机器上构建可查看输出日志
|
||||||
|
|
||||||
|
## 客户端
|
||||||
|
|
||||||
|
> **构建客户端服务, 后台运行** `nohup ./easynode-server &`
|
||||||
|
|
||||||
|
> 功能:服务器基本信息【ssh信息保存在主服务器】
|
@ -12,19 +12,23 @@ const getCertificate =() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
domain: '', // 域名(必须配置, 跨域使用[不配置将所有域名可访问api])
|
domain: 'xxx.com', // https域名, 可不配置
|
||||||
httpPort: 8082,
|
httpPort: 8082,
|
||||||
httpsPort: 8083,
|
httpsPort: 8083,
|
||||||
clientPort: 22022, // 勿更改
|
clientPort: 22022, // 勿更改
|
||||||
certificate: getCertificate(),
|
certificate: getCertificate(),
|
||||||
uploadDir: path.join(process.cwd(),'./app/static/upload'),
|
uploadDir: path.join(process.cwd(),'app/static/upload'),
|
||||||
staticDir: path.join(process.cwd(),'./app/static'),
|
staticDir: path.join(process.cwd(),'app/static'),
|
||||||
sshRecordPath: path.join(__dirname,'./storage/ssh-record.json'),
|
sftpCacheDir: path.join(process.cwd(),'app/socket/.sftp-cache'),
|
||||||
keyPath: path.join(__dirname,'./storage/key.json'),
|
sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'),
|
||||||
hostListPath: path.join(__dirname,'./storage/host-list.json'),
|
keyPath: path.join(process.cwd(),'app/storage/key.json'),
|
||||||
|
hostListPath: path.join(process.cwd(),'app/storage/host-list.json'),
|
||||||
|
emailPath: path.join(process.cwd(),'app/storage/email.json'),
|
||||||
|
notifyPath: path.join(process.cwd(),'app/storage/notify.json'),
|
||||||
|
groupPath: path.join(process.cwd(),'app/storage/group.json'),
|
||||||
apiPrefix: '/api/v1',
|
apiPrefix: '/api/v1',
|
||||||
logConfig: {
|
logConfig: {
|
||||||
outDir: path.join(process.cwd(),'./app/logs'),
|
outDir: path.join(process.cwd(),'./app/logs'),
|
||||||
flag: false // 是否记录日志
|
recordLog: false // 是否记录日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"host": "localhost",
|
|
||||||
"name": "test"
|
|
||||||
}
|
|
||||||
]
|
|
59
server/app/controller/group.js
Normal file
59
server/app/controller/group.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const { readGroupList, writeGroupList, readHostList, writeHostList,randomStr } = require('../utils')
|
||||||
|
|
||||||
|
function getGroupList({ res }) {
|
||||||
|
const data = readGroupList()
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGroupList = async ({ res, request }) => {
|
||||||
|
let { body: { name, index } } = request
|
||||||
|
if(!name) return res.fail({ data: false, msg: '参数错误' })
|
||||||
|
let groupList = readGroupList()
|
||||||
|
let group = { id: randomStr(), name, index }
|
||||||
|
groupList.push(group)
|
||||||
|
groupList.sort((a, b) => a.index - b.index)
|
||||||
|
writeGroupList(groupList)
|
||||||
|
res.success({ data: '新增成功' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGroupList = async ({ res, request }) => {
|
||||||
|
let { params: { id } } = request
|
||||||
|
let { body: { name, index } } = request
|
||||||
|
if(!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
||||||
|
let groupList = readGroupList()
|
||||||
|
let idx = groupList.findIndex(item => item.id === id)
|
||||||
|
let group = { id, name, index }
|
||||||
|
if(idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` })
|
||||||
|
groupList.splice(idx, 1, group)
|
||||||
|
groupList.sort((a, b) => a.index - b.index)
|
||||||
|
writeGroupList(groupList)
|
||||||
|
res.success({ data: '修改成功' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGroup = async ({ res, request }) => {
|
||||||
|
let { params: { id } } = request
|
||||||
|
if(id ==='default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
||||||
|
let groupList = readGroupList()
|
||||||
|
let idx = groupList.findIndex(item => item.id === id)
|
||||||
|
if(idx === -1) return res.fail({ msg: '分组不存在' })
|
||||||
|
|
||||||
|
// 移除分组将所有该分组下host分配到default中去
|
||||||
|
let hostList = readHostList()
|
||||||
|
hostList = hostList.map((item) => {
|
||||||
|
if(item.group === groupList[idx].id) item.group = 'default'
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
writeHostList(hostList)
|
||||||
|
|
||||||
|
groupList.splice(idx, 1)
|
||||||
|
writeGroupList(groupList)
|
||||||
|
|
||||||
|
res.success({ data: '移除成功' })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addGroupList,
|
||||||
|
getGroupList,
|
||||||
|
updateGroupList,
|
||||||
|
removeGroup
|
||||||
|
}
|
@ -6,22 +6,22 @@ function getHostList({ res }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveHost({ res, request }) {
|
function saveHost({ res, request }) {
|
||||||
let { body: { host: newHost, name } } = request
|
let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request
|
||||||
if(!newHost || !name) return res.fail({ msg: '参数错误' })
|
if(!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||||
hostList.push({ host: newHost, name })
|
hostList.push({ host: newHost, name, expired, expiredNotify, group, consoleUrl, remark })
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHost({ res, request }) {
|
function updateHost({ res, request }) {
|
||||||
let { body: { host: newHost, name: newName, oldHost } } = request
|
let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request
|
||||||
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
||||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
||||||
hostList.splice(targetIdx, 1, { name: newName, host: newHost })
|
hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark })
|
||||||
writeHostList(hostList)
|
writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
89
server/app/controller/notify.js
Normal file
89
server/app/controller/notify.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const {
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
emailTransporter,
|
||||||
|
readNotifyList,
|
||||||
|
writeNotifyList } = require('../utils')
|
||||||
|
const commonTemp = require('../template/commonTemp')
|
||||||
|
|
||||||
|
function getSupportEmailList({ res }) {
|
||||||
|
const data = readSupportEmailList()
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserEmailList({ res }) {
|
||||||
|
const userEmailList = readUserEmailList().map(({ target, auth: { user } }) => ({ target, user }))
|
||||||
|
const supportEmailList = readSupportEmailList()
|
||||||
|
const data = userEmailList.map(({ target: userTarget, user: email }) => {
|
||||||
|
let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name
|
||||||
|
return { name, email }
|
||||||
|
})
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushEmail({ res, request }) {
|
||||||
|
let { body: { toEmail, isTest } } = request
|
||||||
|
if(!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' })
|
||||||
|
consola.info('发送测试邮件:', toEmail)
|
||||||
|
let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') })
|
||||||
|
msg = msg && msg.message || msg
|
||||||
|
if(code === 0) return res.success({ msg })
|
||||||
|
return res.fail({ msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserEmailList({ res, request }) {
|
||||||
|
let { body: { target, auth } } = request
|
||||||
|
const supportList = readSupportEmailList()
|
||||||
|
let flag = supportList.some((item) => item.target === target)
|
||||||
|
if(!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` })
|
||||||
|
if(!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' })
|
||||||
|
|
||||||
|
let newUserEmail = { target, auth }
|
||||||
|
let userEmailList = readUserEmailList()
|
||||||
|
let idx = userEmailList.findIndex(({ auth: { user } }) => auth.user === user)
|
||||||
|
if(idx !== -1) userEmailList.splice(idx, 1, newUserEmail)
|
||||||
|
else userEmailList.unshift(newUserEmail)
|
||||||
|
|
||||||
|
const { code, msg } = writeUserEmailList(userEmailList)
|
||||||
|
if(code === 0) return res.success()
|
||||||
|
return res.fail({ msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUserEmail({ res, request }) {
|
||||||
|
let { params: { email } } = request
|
||||||
|
const userEmailList = readUserEmailList()
|
||||||
|
let idx = userEmailList.findIndex(({ auth: { user } }) => user === email)
|
||||||
|
if(idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` })
|
||||||
|
userEmailList.splice(idx, 1)
|
||||||
|
const { code, msg } = writeUserEmailList(userEmailList)
|
||||||
|
if(code === 0) return res.success()
|
||||||
|
return res.fail({ msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotifyList({ res }) {
|
||||||
|
const data = readNotifyList()
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotifyList({ res, request }) {
|
||||||
|
let { body: { type, sw } } = request
|
||||||
|
if(!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
||||||
|
const notifyList = readNotifyList()
|
||||||
|
let target = notifyList.find((item) => item.type === type)
|
||||||
|
if(!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` })
|
||||||
|
target.sw = sw
|
||||||
|
console.log(notifyList)
|
||||||
|
writeNotifyList(notifyList)
|
||||||
|
res.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
pushEmail,
|
||||||
|
getSupportEmailList,
|
||||||
|
getUserEmailList,
|
||||||
|
updateUserEmailList,
|
||||||
|
removeUserEmail,
|
||||||
|
getNotifyList,
|
||||||
|
updateNotifyList
|
||||||
|
}
|
@ -13,7 +13,7 @@ const updateSSH = async ({ res, request }) => {
|
|||||||
else
|
else
|
||||||
sshRecord.splice(idx, 1, record)
|
sshRecord.splice(idx, 1, record)
|
||||||
writeSSHRecord(sshRecord)
|
writeSSHRecord(sshRecord)
|
||||||
console.log('新增凭证:', host)
|
consola.info('新增凭证:', host)
|
||||||
res.success({ data: '保存成功' })
|
res.success({ data: '保存成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ const removeSSH = async ({ res, request }) => {
|
|||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
let idx = sshRecord.findIndex(item => item.host === host)
|
||||||
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
||||||
sshRecord.splice(idx, 1)
|
sshRecord.splice(idx, 1)
|
||||||
console.log('移除凭证:', host)
|
consola.info('移除凭证:', host)
|
||||||
writeSSHRecord(sshRecord)
|
writeSSHRecord(sshRecord)
|
||||||
res.success({ data: '移除成功' })
|
res.success({ data: '移除成功' })
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ const existSSH = async ({ res, request }) => {
|
|||||||
let { body: { host } } = request
|
let { body: { host } } = request
|
||||||
let sshRecord = readSSHRecord()
|
let sshRecord = readSSHRecord()
|
||||||
let idx = sshRecord.findIndex(item => item.host === host)
|
let idx = sshRecord.findIndex(item => item.host === host)
|
||||||
console.log('查询凭证:', host)
|
consola.info('查询凭证:', host)
|
||||||
if(idx === -1) return res.success({ data: false }) // host不存在
|
if(idx === -1) return res.success({ data: false }) // host不存在
|
||||||
res.success({ data: true }) // 存在
|
res.success({ data: true }) // 存在
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ const getCommand = async ({ res, request }) => {
|
|||||||
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
||||||
let sshRecord = readSSHRecord()
|
let sshRecord = readSSHRecord()
|
||||||
let record = sshRecord.find(item => item.host === host)
|
let record = sshRecord.find(item => item.host === host)
|
||||||
console.log('查询登录后执行的指令:', host)
|
consola.info('查询登录后执行的指令:', host)
|
||||||
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
||||||
const { command } = record
|
const { command } = record
|
||||||
if(!command) return res.success({ data: false }) // command不存在
|
if(!command) return res.success({ data: false }) // command不存在
|
@ -1,5 +1,5 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt } = require('../utils')
|
const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils')
|
||||||
|
|
||||||
const getpublicKey = ({ res }) => {
|
const getpublicKey = ({ res }) => {
|
||||||
let { publicKey: data } = readKey()
|
let { publicKey: data } = readKey()
|
||||||
@ -7,34 +7,59 @@ const getpublicKey = ({ res }) => {
|
|||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateTokenAndRecordIP = async (clientIp) => {
|
let timer = null
|
||||||
console.log('密码校验成功, 准备生成token')
|
const allowErrCount = 5 // 允许错误的次数
|
||||||
let { commonKey, jwtExpires } = readKey()
|
const forbidTimer = 60 * 5 // 禁止登录时间
|
||||||
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
let loginErrCount = 0 // 每一轮的登录错误次数
|
||||||
token = AESEncrypt(token) // 对称加密token后再传输给前端
|
let loginErrTotal = 0 // 总的登录错误次数
|
||||||
console.log('aes对称加密token::', token)
|
let loginCountDown = forbidTimer
|
||||||
|
let forbidLogin = false
|
||||||
// 记录客户端登录IP用于判断是否异地(只保留最近10条)
|
|
||||||
const localNetIPInfo = await getNetIPInfo(clientIp)
|
|
||||||
global.loginRecord.unshift(localNetIPInfo)
|
|
||||||
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
|
|
||||||
return { token, jwtExpires }
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = async ({ res, request }) => {
|
const login = async ({ res, request }) => {
|
||||||
let { body: { ciphertext }, ip: clientIp } = request
|
let { body: { ciphertext, jwtExpires }, ip: clientIp } = request
|
||||||
if(!ciphertext) return res.fail({ msg: '参数错误' })
|
if(!ciphertext) return res.fail({ msg: '参数错误' })
|
||||||
|
|
||||||
|
if(forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
||||||
|
|
||||||
|
loginErrCount++
|
||||||
|
loginErrTotal++
|
||||||
|
if(loginErrCount >= allowErrCount) {
|
||||||
|
const { ip, country, city } = await getNetIPInfo(clientIp)
|
||||||
|
// 发送通知&禁止登录
|
||||||
|
let sw = getNotifySwByType('err_login')
|
||||||
|
if(sw) sendEmailToConfList('登录错误提醒', `重新登录次数: ${ loginErrTotal }<br/>地点:${ country + city }<br/>IP: ${ ip }`)
|
||||||
|
forbidLogin = true
|
||||||
|
loginErrCount = 0
|
||||||
|
|
||||||
|
// forbidTimer秒后解禁
|
||||||
|
setTimeout(() => {
|
||||||
|
forbidLogin = false
|
||||||
|
}, loginCountDown * 1000)
|
||||||
|
|
||||||
|
// 计算登录倒计时
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if(loginCountDown <= 0){
|
||||||
|
clearInterval(timer)
|
||||||
|
loginCountDown = forbidTimer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginCountDown--
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录流程
|
||||||
try {
|
try {
|
||||||
console.log('ciphertext', ciphertext)
|
// console.log('ciphertext', ciphertext)
|
||||||
let password = RSADecrypt(ciphertext)
|
let password = RSADecrypt(ciphertext)
|
||||||
|
// console.log('Decrypt解密password:', password)
|
||||||
let { pwd } = readKey()
|
let { pwd } = readKey()
|
||||||
if(password === 'admin' && pwd === 'admin') {
|
if(password === 'admin' && pwd === 'admin') {
|
||||||
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
|
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
|
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
|
||||||
}
|
}
|
||||||
password = SHA1Encrypt(password)
|
password = SHA1Encrypt(password)
|
||||||
if(password !== pwd) return res.fail({ msg: '密码错误' })
|
if(password !== pwd) return res.fail({ msg: '密码错误' })
|
||||||
const { token, jwtExpires } = await generateTokenAndRecordIP(clientIp)
|
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
|
return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('解密失败:', error)
|
console.log('解密失败:', error)
|
||||||
@ -42,6 +67,28 @@ const login = async ({ res, request }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
||||||
|
loginErrCount = loginErrTotal = 0 // 登录成功, 清空错误次数
|
||||||
|
|
||||||
|
// consola.success('登录成功, 准备生成token', new Date())
|
||||||
|
// 生产token
|
||||||
|
let { commonKey } = readKey()
|
||||||
|
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
||||||
|
token = AESEncrypt(token) // 对称加密token后再传输给前端
|
||||||
|
|
||||||
|
// 记录客户端登录IP(用于判断是否异地且只保留最近10条)
|
||||||
|
const clientIPInfo = await getNetIPInfo(clientIp)
|
||||||
|
const { ip, country, city } = clientIPInfo || {}
|
||||||
|
|
||||||
|
// 邮件登录通知
|
||||||
|
let sw = getNotifySwByType('login')
|
||||||
|
if(sw) sendEmailToConfList('登录提醒', `地点:${ country + city }<br/>IP: ${ ip }`)
|
||||||
|
|
||||||
|
global.loginRecord.unshift(clientIPInfo)
|
||||||
|
if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
const updatePwd = async ({ res, request }) => {
|
const updatePwd = async ({ res, request }) => {
|
||||||
let { body: { oldPwd, newPwd } } = request
|
let { body: { oldPwd, newPwd } } = request
|
||||||
let rsaOldPwd = RSADecrypt(oldPwd)
|
let rsaOldPwd = RSADecrypt(oldPwd)
|
||||||
@ -49,9 +96,13 @@ const updatePwd = async ({ res, request }) => {
|
|||||||
let keyObj = readKey()
|
let keyObj = readKey()
|
||||||
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
||||||
// 旧密钥校验通过,加密保存新密码
|
// 旧密钥校验通过,加密保存新密码
|
||||||
newPwd = SHA1Encrypt(RSADecrypt(newPwd))
|
newPwd = RSADecrypt(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(RSADecrypt(newPwd))
|
||||||
keyObj.pwd = newPwd
|
keyObj.pwd = newPwd
|
||||||
writeKey(keyObj)
|
writeKey(keyObj)
|
||||||
|
|
||||||
|
let sw = getNotifySwByType('updatePwd')
|
||||||
|
if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改')
|
||||||
|
|
||||||
res.success({ data: true, msg: 'success' })
|
res.success({ data: true, msg: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
|
|
||||||
const NodeRSA = require('node-rsa')
|
const NodeRSA = require('node-rsa')
|
||||||
|
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
|
||||||
|
|
||||||
const isDev = !isProd()
|
const isDev = !isProd()
|
||||||
|
|
||||||
// 存储本机IP, 供host列表接口调用
|
// 存储本机IP, 供host列表接口调用
|
||||||
async function initIp() {
|
async function initLocalIp() {
|
||||||
if(isDev) return console.log('非生产环境不初始化保存本地IP')
|
if(isDev) return consola.info('非生产环境不初始化保存本地IP')
|
||||||
const localNetIPInfo = await getNetIPInfo()
|
const localNetIPInfo = await getNetIPInfo()
|
||||||
let vpsList = readHostList()
|
let vpsList = readHostList()
|
||||||
let { ip: localNetIP } = localNetIPInfo
|
let { ip: localNetIP } = localNetIPInfo
|
||||||
if(vpsList.some(({ host }) => host === localNetIP)) return console.log('本机IP已储存: ', localNetIP)
|
if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP)
|
||||||
vpsList.unshift({ name: 'server-side-host', host: localNetIP })
|
vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' })
|
||||||
writeHostList(vpsList)
|
writeHostList(vpsList)
|
||||||
console.log('Task: 生产环境首次启动储存本机IP: ', localNetIP)
|
consola.info('Task: 生产环境首次启动储存本机IP: ', localNetIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
||||||
async function initRsa() {
|
async function initRsa() {
|
||||||
let keyObj = readKey()
|
let keyObj = readKey()
|
||||||
if(keyObj.privateKey && keyObj.publicKey) return console.log('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
||||||
|
|
||||||
let key = new NodeRSA({ b: 1024 })
|
let key = new NodeRSA({ b: 1024 })
|
||||||
key.setOptions({ encryptionScheme: 'pkcs1' })
|
key.setOptions({ encryptionScheme: 'pkcs1' })
|
||||||
@ -27,22 +27,22 @@ async function initRsa() {
|
|||||||
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
|
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
|
||||||
keyObj.publicKey = publicKey // 公开公钥
|
keyObj.publicKey = publicKey // 公开公钥
|
||||||
writeKey(keyObj)
|
writeKey(keyObj)
|
||||||
console.log('Task: 已生成新的非对称加密公私钥')
|
consola.info('Task: 已生成新的非对称加密公私钥')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机的commonKey secret
|
// 随机的commonKey secret
|
||||||
function randomJWTSecret() {
|
function randomJWTSecret() {
|
||||||
let keyObj = readKey()
|
let keyObj = readKey()
|
||||||
if(keyObj.commonKey) return console.log('commonKey密钥已存在')
|
if(keyObj.commonKey) return consola.info('commonKey密钥已存在')
|
||||||
|
|
||||||
keyObj.commonKey = randomStr(16)
|
keyObj.commonKey = randomStr(16)
|
||||||
writeKey(keyObj)
|
writeKey(keyObj)
|
||||||
console.log('Task: 已生成新的随机commonKey密钥')
|
consola.info('Task: 已生成新的随机commonKey密钥')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
randomJWTSecret() // 先生成全局唯一密钥
|
randomJWTSecret() // 先生成全局唯一密钥
|
||||||
initIp()
|
initLocalIp()
|
||||||
initRsa()
|
initRsa()
|
||||||
// 用于记录客户端登录IP的列表
|
// 用于记录客户端登录IP的列表
|
||||||
global.loginRecord = []
|
global.loginRecord = []
|
||||||
|
0
server/app/logs/.gitkeep
Normal file
0
server/app/logs/.gitkeep
Normal file
@ -1,5 +1,10 @@
|
|||||||
|
const consola = require('consola')
|
||||||
|
global.consola = consola
|
||||||
const { httpServer, httpsServer, clientHttpServer } = require('./server')
|
const { httpServer, httpsServer, clientHttpServer } = require('./server')
|
||||||
const initLocal = require('./init')
|
const initLocal = require('./init')
|
||||||
|
const scheduleJob = require('./schedule')
|
||||||
|
|
||||||
|
scheduleJob()
|
||||||
|
|
||||||
initLocal()
|
initLocal()
|
||||||
|
|
||||||
|
@ -5,12 +5,11 @@ let whitePath = [
|
|||||||
'/login',
|
'/login',
|
||||||
'/get-pub-pem'
|
'/get-pub-pem'
|
||||||
].map(item => (apiPrefix + item))
|
].map(item => (apiPrefix + item))
|
||||||
console.log('路由白名单:', whitePath)
|
consola.info('路由白名单:', whitePath)
|
||||||
|
|
||||||
const useAuth = async ({ request, res }, next) => {
|
const useAuth = async ({ request, res }, next) => {
|
||||||
const { path, headers: { token } } = request
|
const { path, headers: { token } } = request
|
||||||
console.log('path: ', path)
|
consola.info('verify path: ', path)
|
||||||
// console.log('token: ', token)
|
|
||||||
if(whitePath.includes(path)) return next()
|
if(whitePath.includes(path)) return next()
|
||||||
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
||||||
// 验证token
|
// 验证token
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
const koaBody = require('koa-body')
|
const koaBody = require('koa-body')
|
||||||
const { uploadDir } = require('../config')
|
const { uploadDir } = require('../config')
|
||||||
|
|
||||||
module.exports = koaBody({
|
module.exports = koaBody({
|
||||||
multipart: true, // 支持 multipart-formdate 的表单
|
multipart: true, // 支持 multipart-formdate 的表单
|
||||||
formidable: {
|
formidable: {
|
||||||
uploadDir, // 上传目录
|
uploadDir, // 上传目录
|
||||||
keepExtensions: true, // 保持文件的后缀
|
keepExtensions: true, // 保持文件的后缀
|
||||||
multipart: true, // 多文件上传
|
multipart: true, // 多文件上传
|
||||||
maxFieldsSize: 2 * 1024 * 1024, // 文件上传大小 单位:B
|
maxFieldsSize: 2 * 1024 * 1024 // 文件上传大小 单位:B
|
||||||
onFileBegin: (name, file) => { // 文件上传前的设置
|
}
|
||||||
// console.log(`name: ${name}`)
|
|
||||||
// console.log(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
@ -1,10 +1,11 @@
|
|||||||
const cors = require('@koa/cors')
|
const cors = require('@koa/cors')
|
||||||
const { domain } = require('../config')
|
// const { domain } = require('../config')
|
||||||
|
|
||||||
// 跨域处理
|
// 跨域处理
|
||||||
const useCors = cors({
|
const useCors = cors({
|
||||||
origin: ({ req }) => {
|
origin: ({ req }) => {
|
||||||
return domain || req.headers.origin
|
// return domain || req.headers.origin
|
||||||
|
return req.headers.origin
|
||||||
},
|
},
|
||||||
credentials: true,
|
credentials: true,
|
||||||
allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]
|
allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const responseHandler = require('./response') // 统一返回格式, 错误捕获
|
const responseHandler = require('./response') // 统一返回格式, 错误捕获
|
||||||
const useAuth = require('./auth') // 鉴权
|
const useAuth = require('./auth') // 鉴权
|
||||||
const useCors = require('./cors') // 处理跨域
|
// const useCors = require('./cors') // 处理跨域[暂时禁止]
|
||||||
const useLog = require('./log4') // 记录日志,需要等待路由处理完成,所以得放路由前
|
const useLog = require('./log4') // 记录日志,需要等待路由处理完成,所以得放路由前
|
||||||
const useKoaBody = require('./body') // 处理body参数 【请求需先走该中间件】
|
const useKoaBody = require('./body') // 处理body参数 【请求需先走该中间件】
|
||||||
const { useRoutes, useAllowedMethods } = require('./router') // 路由管理
|
const { useRoutes, useAllowedMethods } = require('./router') // 路由管理
|
||||||
@ -13,7 +13,7 @@ module.exports = [
|
|||||||
compress,
|
compress,
|
||||||
history,
|
history,
|
||||||
useStatic, // staic先注册,不然会被jwt拦截
|
useStatic, // staic先注册,不然会被jwt拦截
|
||||||
useCors,
|
// useCors,
|
||||||
responseHandler,
|
responseHandler,
|
||||||
useKoaBody, // 先处理body,log和router都要用到
|
useKoaBody, // 先处理body,log和router都要用到
|
||||||
useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api, 设置保存日志的目录需使用process.cwd()】
|
useLog, // 日志记录开始【该module使用到了fs.mkdir()等读写api, 设置保存日志的目录需使用process.cwd()】
|
||||||
|
@ -1,58 +1,58 @@
|
|||||||
const log4js = require('log4js')
|
const log4js = require('log4js')
|
||||||
const { outDir, flag } = require('../config').logConfig
|
const { outDir, recordLog } = require('../config').logConfig
|
||||||
|
|
||||||
log4js.configure({
|
log4js.configure({
|
||||||
appenders: {
|
appenders: {
|
||||||
// 控制台输出
|
// 控制台输出
|
||||||
out: {
|
out: {
|
||||||
type: 'stdout',
|
type: 'stdout',
|
||||||
layout: {
|
layout: {
|
||||||
type: 'colored'
|
type: 'colored'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 保存日志文件
|
// 保存日志文件
|
||||||
cheese: {
|
cheese: {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
|
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
|
||||||
filename: `${ outDir }/receive.log`
|
filename: `${ outDir }/receive.log`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
default: {
|
default: {
|
||||||
appenders: [ 'out', 'cheese' ], // 配置
|
appenders: [ 'out', 'cheese' ], // 配置
|
||||||
level: 'info' // 只输出info以上级别的日志
|
level: 'info' // 只输出info以上级别的日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pm2: true
|
// pm2: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const logger = log4js.getLogger()
|
const logger = log4js.getLogger()
|
||||||
|
|
||||||
const useLog = () => {
|
const useLog = () => {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const { method, path, origin, query, body, headers, ip } = ctx.request
|
const { method, path, origin, query, body, headers, ip } = ctx.request
|
||||||
const data = {
|
const data = {
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
origin,
|
origin,
|
||||||
query,
|
query,
|
||||||
body,
|
body,
|
||||||
ip,
|
ip,
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
await next() // 等待路由处理完成,再开始记录日志
|
await next() // 等待路由处理完成,再开始记录日志
|
||||||
// 是否记录日志
|
// 是否记录日志
|
||||||
if (flag) {
|
if (recordLog) {
|
||||||
const { status, params } = ctx
|
const { status, params } = ctx
|
||||||
data.status = status
|
data.status = status
|
||||||
data.params = params
|
data.params = params
|
||||||
data.result = ctx.body || 'no content'
|
data.result = ctx.body || 'no content'
|
||||||
if (String(status).startsWith(4) || String(status).startsWith(5))
|
if (String(status).startsWith(4) || String(status).startsWith(5))
|
||||||
logger.error(JSON.stringify(data))
|
logger.error(JSON.stringify(data))
|
||||||
else
|
else
|
||||||
logger.info(JSON.stringify(data))
|
logger.info(JSON.stringify(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = useLog()
|
module.exports = useLog()
|
@ -22,7 +22,7 @@ const responseHandler = async (ctx, next) => {
|
|||||||
try {
|
try {
|
||||||
await next() // 每个中间件都需等待next完成调用,不然会返回404给前端!!!
|
await next() // 每个中间件都需等待next完成调用,不然会返回404给前端!!!
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('中间件错误:', err)
|
consola.error('中间件错误:', err)
|
||||||
if (err.status)
|
if (err.status)
|
||||||
ctx.res.fail({ status: err.status, msg: err.message }) // 自己主动抛出的错误 throwError
|
ctx.res.fail({ status: err.status, msg: err.message }) // 自己主动抛出的错误 throwError
|
||||||
else
|
else
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const router = require('../router')
|
const router = require('../router')
|
||||||
|
|
||||||
// 路由中间件
|
// 路由中间件
|
||||||
const useRoutes = router.routes()
|
const useRoutes = router.routes()
|
||||||
// 优化错误提示中间件
|
// 优化错误提示中间件
|
||||||
// 原先如果请求方法错误响应404
|
// 原先如果请求方法错误响应404
|
||||||
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
|
// 使用该中间件后请求方法错误会提示405 Method Not Allowed【get list ✔200 post /list ❌405】
|
||||||
const useAllowedMethods = router.allowedMethods()
|
const useAllowedMethods = router.allowedMethods()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
useRoutes,
|
useRoutes,
|
||||||
useAllowedMethods
|
useAllowedMethods
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
const koaStatic = require('koa-static')
|
const koaStatic = require('koa-static')
|
||||||
const { staticDir } = require('../config')
|
const { staticDir } = require('../config')
|
||||||
|
|
||||||
const useStatic = koaStatic(staticDir)
|
const useStatic = koaStatic(staticDir, {
|
||||||
|
maxage: 1000 * 60 * 60 * 24 * 30,
|
||||||
module.exports = useStatic
|
gzip: true,
|
||||||
|
setHeaders: (res, path) => {
|
||||||
|
if(path && path.endsWith('.html')) {
|
||||||
|
res.setHeader('Cache-Control', 'max-age=0')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = useStatic
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh-info')
|
const { updateSSH, removeSSH, existSSH, getCommand } = require('../controller/ssh')
|
||||||
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host-info')
|
const { getHostList, saveHost, updateHost, removeHost, updateHostSort } = require('../controller/host')
|
||||||
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
||||||
|
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
|
|
||||||
// 路由统一管理
|
const ssh = [
|
||||||
const routes = [
|
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/update-ssh',
|
path: '/update-ssh',
|
||||||
@ -23,7 +24,9 @@ const routes = [
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/command',
|
path: '/command',
|
||||||
controller: getCommand
|
controller: getCommand
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
const host = [
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/host-list',
|
path: '/host-list',
|
||||||
@ -48,7 +51,9 @@ const routes = [
|
|||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/host-sort',
|
path: '/host-sort',
|
||||||
controller: updateHostSort
|
controller: updateHostSort
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
const user = [
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/get-pub-pem',
|
path: '/get-pub-pem',
|
||||||
@ -70,5 +75,65 @@ const routes = [
|
|||||||
controller: getLoginRecord
|
controller: getLoginRecord
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const notify = [
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/support-email',
|
||||||
|
controller: getSupportEmailList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/user-email',
|
||||||
|
controller: getUserEmailList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/push-email',
|
||||||
|
controller: pushEmail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/user-email',
|
||||||
|
controller: updateUserEmailList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'delete',
|
||||||
|
path: '/user-email/:email',
|
||||||
|
controller: removeUserEmail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/notify',
|
||||||
|
controller: getNotifyList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'put',
|
||||||
|
path: '/notify',
|
||||||
|
controller: updateNotifyList
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
module.exports = routes
|
const group = [
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/group',
|
||||||
|
controller: getGroupList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/group',
|
||||||
|
controller: addGroupList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'delete',
|
||||||
|
path: '/group/:id',
|
||||||
|
controller: removeGroup
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'put',
|
||||||
|
path: '/group/:id',
|
||||||
|
controller: updateGroupList
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = [].concat(ssh, host, user, notify, group)
|
||||||
|
29
server/app/schedule/expired-notify.js
Normal file
29
server/app/schedule/expired-notify.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const schedule = require('node-schedule')
|
||||||
|
const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils')
|
||||||
|
|
||||||
|
const expiredNotifyJob = () => {
|
||||||
|
consola.info('=====开始检测服务器到期时间=====', new Date())
|
||||||
|
const hostList = readHostList()
|
||||||
|
for (const item of hostList) {
|
||||||
|
if(!item.expiredNotify) continue
|
||||||
|
const { host, name, expired, consoleUrl } = item
|
||||||
|
const restDay = Number(((expired - Date.now()) / (1000 * 60 * 60 * 24)).toFixed(1))
|
||||||
|
console.log(Date.now(), restDay)
|
||||||
|
let title = '服务器到期提醒'
|
||||||
|
let content = `别名: ${ name }<br/>IP: ${ host }<br/>到期时间:${ formatTimestamp(expired, 'week') }<br/>控制台: ${ consoleUrl || '未填写' }`
|
||||||
|
if(0 <= restDay && restDay <= 1) {
|
||||||
|
let temp = '有服务器将在一天后到期,请关注<br/>'
|
||||||
|
sendEmailToConfList(title, temp + content)
|
||||||
|
}else if(3 <= restDay && restDay < 4) {
|
||||||
|
let temp = '有服务器将在三天后到期,请关注<br/>'
|
||||||
|
sendEmailToConfList(title, temp + content)
|
||||||
|
}else if(7 <= restDay && restDay < 8) {
|
||||||
|
let temp = '有服务器将在七天后到期,请关注<br/>'
|
||||||
|
sendEmailToConfList(title, temp + content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
|
||||||
|
}
|
7
server/app/schedule/index.js
Normal file
7
server/app/schedule/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const offlineInspect = require('./offline-inspect')
|
||||||
|
const expiredNotify = require('./expired-notify')
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
offlineInspect()
|
||||||
|
expiredNotify()
|
||||||
|
}
|
39
server/app/schedule/offline-inspect.js
Normal file
39
server/app/schedule/offline-inspect.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const schedule = require('node-schedule')
|
||||||
|
const { clientPort } = require('../config')
|
||||||
|
const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, isProd } = require('../utils')
|
||||||
|
const testConnectAsync = require('../utils/test-connect')
|
||||||
|
|
||||||
|
let sendNotifyRecord = new Map()
|
||||||
|
const offlineJob = () => {
|
||||||
|
let sw = getNotifySwByType('host_offline')
|
||||||
|
if(!sw) return
|
||||||
|
consola.info('=====开始检测服务器状态=====', new Date())
|
||||||
|
for (const item of readHostList()) {
|
||||||
|
const { host, name } = item
|
||||||
|
// consola.info('start inpect:', host, name )
|
||||||
|
testConnectAsync({
|
||||||
|
port: clientPort ,
|
||||||
|
host: `http://${ host }`,
|
||||||
|
timeout: 3000,
|
||||||
|
retryTimes: 20 // 尝试重连次数
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// consola.success('测试连接成功:', host, name)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
consola.error('测试连接失败: ', host, name)
|
||||||
|
// 当前小时是否发送过通知
|
||||||
|
let curHourIsSend = sendNotifyRecord.has(host) && (sendNotifyRecord.get(host).sendTime === formatTimestamp(Date.now(), 'hour'))
|
||||||
|
if(curHourIsSend) return consola.info('当前小时已发送过通知: ', sendNotifyRecord.get(host).sendTime)
|
||||||
|
sendEmailToConfList('服务器离线提醒', `别名: ${ name }<br/>IP: ${ host }<br/>错误信息:${ error.message }`)
|
||||||
|
.then(() => {
|
||||||
|
sendNotifyRecord.set(host, { 'sendTime': formatTimestamp(Date.now(), 'hour') })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
if(!isProd()) return consola.info('本地开发不检测服务器离线状态')
|
||||||
|
schedule.scheduleJob('0 0/5 12 1/1 * ?', offlineJob)
|
||||||
|
}
|
@ -1,67 +1,68 @@
|
|||||||
const Koa = require('koa')
|
const Koa = require('koa')
|
||||||
const compose = require('koa-compose') // 组合中间件,简化写法
|
const compose = require('koa-compose') // 组合中间件,简化写法
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
const { clientPort } = require('./config')
|
const { clientPort } = require('./config')
|
||||||
const { domain, httpPort, httpsPort, certificate } = require('./config')
|
const { domain, httpPort, httpsPort, certificate } = require('./config')
|
||||||
const middlewares = require('./middlewares')
|
const middlewares = require('./middlewares')
|
||||||
const wsMonitorOsInfo = require('./socket/monitor')
|
const wsMonitorOsInfo = require('./socket/monitor')
|
||||||
const wsTerminal = require('./socket/terminal')
|
const wsTerminal = require('./socket/terminal')
|
||||||
const wsHostStatus = require('./socket/host-status')
|
const wsSftp = require('./socket/sftp')
|
||||||
const wsClientInfo = require('./socket/clients')
|
const wsHostStatus = require('./socket/host-status')
|
||||||
const { throwError } = require('./utils')
|
const wsClientInfo = require('./socket/clients')
|
||||||
|
const { throwError } = require('./utils')
|
||||||
const httpServer = () => {
|
|
||||||
// if(EXEC_ENV === 'production') return console.log('========生成环境不创建http服务========')
|
const httpServer = () => {
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
serverHandler(app, server)
|
serverHandler(app, server)
|
||||||
// ws一直报跨域的错误:参照官方文档使用createServer API创建服务
|
// ws一直报跨域的错误:参照官方文档使用createServer API创建服务
|
||||||
server.listen(httpPort, () => {
|
server.listen(httpPort, () => {
|
||||||
console.log(`Server(http) is running on: http://localhost:${ httpPort }`)
|
consola.success(`Server(http) is running on: http://localhost:${ httpPort }`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpsServer = () => {
|
const httpsServer = () => {
|
||||||
if(!certificate) return console.log('未上传证书, 创建https服务失败')
|
if(!certificate) return consola.error('未上传证书, 创建https服务失败')
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
const server = https.createServer(certificate, app.callback())
|
const server = https.createServer(certificate, app.callback())
|
||||||
serverHandler(app, server)
|
serverHandler(app, server)
|
||||||
server.listen(httpsPort, (err) => {
|
server.listen(httpsPort, (err) => {
|
||||||
if (err) return console.log('https server error: ', err)
|
if (err) return consola.error('https server error: ', err)
|
||||||
console.log(`Server(https) is running: https://${ domain }:${ httpsPort }`)
|
consola.success(`Server(https) is running: https://${ domain }:${ httpsPort }`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientHttpServer = () => {
|
const clientHttpServer = () => {
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
wsMonitorOsInfo(server) // 监控本机信息
|
wsMonitorOsInfo(server) // 监控本机信息
|
||||||
server.listen(clientPort, () => {
|
server.listen(clientPort, () => {
|
||||||
console.log(`Client(http) is running on: http://localhost:${ clientPort }`)
|
consola.success(`Client(http) is running on: http://localhost:${ clientPort }`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务
|
// 服务
|
||||||
function serverHandler(app, server) {
|
function serverHandler(app, server) {
|
||||||
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
||||||
wsTerminal(server) // 终端
|
wsTerminal(server) // 终端
|
||||||
wsHostStatus(server) // 终端侧边栏host信息
|
wsSftp(server) // sftp
|
||||||
wsClientInfo(server) // 客户端信息
|
wsHostStatus(server) // 终端侧边栏host信息
|
||||||
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
wsClientInfo(server) // 客户端信息
|
||||||
app.use(compose(middlewares))
|
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
||||||
// 捕获error.js模块抛出的服务错误
|
app.use(compose(middlewares))
|
||||||
app.on('error', (err, ctx) => {
|
// 捕获error.js模块抛出的服务错误
|
||||||
ctx.status = 500
|
app.on('error', (err, ctx) => {
|
||||||
ctx.body = {
|
ctx.status = 500
|
||||||
status: ctx.status,
|
ctx.body = {
|
||||||
message: `Program Error:${ err.message }`
|
status: ctx.status,
|
||||||
}
|
message: `Program Error:${ err.message }`
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
module.exports = {
|
|
||||||
httpServer,
|
module.exports = {
|
||||||
httpsServer,
|
httpServer,
|
||||||
clientHttpServer
|
httpsServer,
|
||||||
|
clientHttpServer
|
||||||
}
|
}
|
0
server/app/socket/.sftp-cache/.gitkeep
Normal file
0
server/app/socket/.sftp-cache/.gitkeep
Normal file
@ -9,7 +9,7 @@ let clientSockets = {}, clientsData = {}
|
|||||||
function getClientsInfo(socketId) {
|
function getClientsInfo(socketId) {
|
||||||
let hostList = readHostList()
|
let hostList = readHostList()
|
||||||
hostList
|
hostList
|
||||||
.map(({ host }) => {
|
.map(({ host, name }) => {
|
||||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
@ -21,13 +21,14 @@ function getClientsInfo(socketId) {
|
|||||||
clientSockets[socketId].push(clientSocket)
|
clientSockets[socketId].push(clientSocket)
|
||||||
return {
|
return {
|
||||||
host,
|
host,
|
||||||
|
name,
|
||||||
clientSocket
|
clientSocket
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(({ host, clientSocket }) => {
|
.map(({ host, name, clientSocket }) => {
|
||||||
clientSocket
|
clientSocket
|
||||||
.on('connect', () => {
|
.on('connect', () => {
|
||||||
console.log('client connect success:', host)
|
consola.success('client connect success:', host, name)
|
||||||
clientSocket.on('client_data', (osData) => {
|
clientSocket.on('client_data', (osData) => {
|
||||||
clientsData[host] = osData
|
clientsData[host] = osData
|
||||||
})
|
})
|
||||||
@ -36,11 +37,11 @@ function getClientsInfo(socketId) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on('connect_error', (error) => {
|
.on('connect_error', (error) => {
|
||||||
console.log('client connect fail:', host, error.message)
|
consola.error('client connect fail:', host, name, error.message)
|
||||||
clientsData[host] = null
|
clientsData[host] = null
|
||||||
})
|
})
|
||||||
.on('disconnect', () => {
|
.on('disconnect', () => {
|
||||||
console.log('client connect disconnect:', host)
|
consola.info('client connect disconnect:', host, name)
|
||||||
clientsData[host] = null
|
clientsData[host] = null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -68,7 +69,7 @@ module.exports = (httpServer) => {
|
|||||||
|
|
||||||
// 收集web端连接的id
|
// 收集web端连接的id
|
||||||
clientSockets[socket.id] = []
|
clientSockets[socket.id] = []
|
||||||
console.log('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
|
consola.info('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
|
||||||
|
|
||||||
// 获取客户端数据
|
// 获取客户端数据
|
||||||
getClientsInfo(socket.id)
|
getClientsInfo(socket.id)
|
||||||
@ -89,7 +90,7 @@ module.exports = (httpServer) => {
|
|||||||
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||||
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
||||||
delete clientSockets[socket.id]
|
delete clientSockets[socket.id]
|
||||||
console.log('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
|
consola.info('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,74 +1,74 @@
|
|||||||
const { Server: ServerIO } = require('socket.io')
|
const { Server: ServerIO } = require('socket.io')
|
||||||
const { io: ClientIO } = require('socket.io-client')
|
const { io: ClientIO } = require('socket.io-client')
|
||||||
const { clientPort } = require('../config')
|
const { clientPort } = require('../config')
|
||||||
const { verifyAuth } = require('../utils')
|
const { verifyAuth } = require('../utils')
|
||||||
|
|
||||||
let hostSockets = {}
|
let hostSockets = {}
|
||||||
|
|
||||||
function getHostInfo(serverSocket, host) {
|
function getHostInfo(serverSocket, host) {
|
||||||
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
forceNew: true,
|
forceNew: false,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
reconnectionDelay: 3000,
|
reconnectionDelay: 3000,
|
||||||
reconnectionAttempts: 100
|
reconnectionAttempts: 100
|
||||||
})
|
})
|
||||||
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
|
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
|
||||||
hostSockets[serverSocket.id] = hostSocket
|
hostSockets[serverSocket.id] = hostSocket
|
||||||
|
|
||||||
hostSocket
|
hostSocket
|
||||||
.on('connect', () => {
|
.on('connect', () => {
|
||||||
console.log('客户端状态socket连接成功:', host)
|
consola.success('host-status-socket连接成功:', host)
|
||||||
hostSocket.on('client_data', (data) => {
|
hostSocket.on('client_data', (data) => {
|
||||||
serverSocket.emit('host_data', data)
|
serverSocket.emit('host_data', data)
|
||||||
})
|
})
|
||||||
hostSocket.on('client_error', () => {
|
hostSocket.on('client_error', () => {
|
||||||
serverSocket.emit('host_data', null)
|
serverSocket.emit('host_data', null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on('connect_error', (error) => {
|
.on('connect_error', (error) => {
|
||||||
console.log('客户端状态socket连接[失败]:', host, error.message)
|
consola.error('host-status-socket连接[失败]:', host, error.message)
|
||||||
serverSocket.emit('host_data', null)
|
serverSocket.emit('host_data', null)
|
||||||
})
|
})
|
||||||
.on('disconnect', () => {
|
.on('disconnect', () => {
|
||||||
console.log('客户端状态socket连接[断开]:', host)
|
consola.info('host-status-socket连接[断开]:', host)
|
||||||
serverSocket.emit('host_data', null)
|
serverSocket.emit('host_data', null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
module.exports = (httpServer) => {
|
||||||
const serverIo = new ServerIO(httpServer, {
|
const serverIo = new ServerIO(httpServer, {
|
||||||
path: '/host-status',
|
path: '/host-status',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*' // 需配置跨域
|
origin: '*' // 需配置跨域
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
serverIo.on('connection', (serverSocket) => {
|
serverIo.on('connection', (serverSocket) => {
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
|
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
|
||||||
serverSocket.on('init_host_data', ({ token, host }) => {
|
serverSocket.on('init_host_data', ({ token, host }) => {
|
||||||
// 校验登录态
|
// 校验登录态
|
||||||
const { code, msg } = verifyAuth(token, clientIp)
|
const { code, msg } = verifyAuth(token, clientIp)
|
||||||
if(code !== 1) {
|
if(code !== 1) {
|
||||||
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
|
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
|
||||||
serverSocket.disconnect()
|
serverSocket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户端数据
|
// 获取客户端数据
|
||||||
getHostInfo(serverSocket, host)
|
getHostInfo(serverSocket, host)
|
||||||
|
|
||||||
console.log('host-socket连接socketId: ', serverSocket.id, 'host-socket已连接数: ', Object.keys(hostSockets).length)
|
consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length)
|
||||||
|
|
||||||
// 关闭连接
|
// 关闭连接
|
||||||
serverSocket.on('disconnect', () => {
|
serverSocket.on('disconnect', () => {
|
||||||
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||||
let socket = hostSockets[serverSocket.id]
|
let socket = hostSockets[serverSocket.id]
|
||||||
socket.close && socket.close()
|
socket.close && socket.close()
|
||||||
delete hostSockets[serverSocket.id]
|
delete hostSockets[serverSocket.id]
|
||||||
console.log('host-socket剩余连接数: ', Object.keys(hostSockets).length)
|
consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,70 +1,71 @@
|
|||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const schedule = require('node-schedule')
|
const schedule = require('node-schedule')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
let getOsData = require('../utils/os-data')
|
let getOsData = require('../utils/os-data')
|
||||||
|
const consola = require('consola')
|
||||||
let serverSockets = {}, ipInfo = {}, osData = {}
|
|
||||||
|
let serverSockets = {}, ipInfo = {}, osData = {}
|
||||||
async function getIpInfo() {
|
|
||||||
try {
|
async function getIpInfo() {
|
||||||
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
try {
|
||||||
console.log('getIpInfo Success: ', new Date())
|
let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN')
|
||||||
ipInfo = data
|
consola.success('getIpInfo Success: ', new Date())
|
||||||
} catch (error) {
|
ipInfo = data
|
||||||
console.log('getIpInfo Error: ', new Date(), error)
|
} catch (error) {
|
||||||
}
|
consola.error('getIpInfo Error: ', new Date(), error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function ipSchedule() {
|
|
||||||
let rule1 = new schedule.RecurrenceRule()
|
function ipSchedule() {
|
||||||
rule1.second = [0, 10, 20, 30, 40, 50]
|
let rule1 = new schedule.RecurrenceRule()
|
||||||
schedule.scheduleJob(rule1, () => {
|
rule1.second = [0, 10, 20, 30, 40, 50]
|
||||||
let { query, country, city } = ipInfo || {}
|
schedule.scheduleJob(rule1, () => {
|
||||||
if(query && country && city) return
|
let { query, country, city } = ipInfo || {}
|
||||||
console.log('Task: start getIpInfo', new Date())
|
if(query && country && city) return
|
||||||
getIpInfo()
|
consola.success('Task: start getIpInfo', new Date())
|
||||||
})
|
getIpInfo()
|
||||||
|
})
|
||||||
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
|
||||||
let rule2 = new schedule.RecurrenceRule()
|
// 每日凌晨两点整,刷新ip信息(兼容动态ip服务器)
|
||||||
rule2.hour = 2
|
let rule2 = new schedule.RecurrenceRule()
|
||||||
rule2.minute = 0
|
rule2.hour = 2
|
||||||
rule2.second = 0
|
rule2.minute = 0
|
||||||
schedule.scheduleJob(rule2, () => {
|
rule2.second = 0
|
||||||
console.log('Task: refresh ip info', new Date())
|
schedule.scheduleJob(rule2, () => {
|
||||||
getIpInfo()
|
consola.info('Task: refresh ip info', new Date())
|
||||||
})
|
getIpInfo()
|
||||||
}
|
})
|
||||||
|
}
|
||||||
ipSchedule()
|
|
||||||
|
ipSchedule()
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new Server(httpServer, {
|
module.exports = (httpServer) => {
|
||||||
path: '/client/os-info',
|
const serverIo = new Server(httpServer, {
|
||||||
cors: {
|
path: '/client/os-info',
|
||||||
origin: '*'
|
cors: {
|
||||||
}
|
origin: '*'
|
||||||
})
|
}
|
||||||
|
})
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 存储对应websocket连接的定时器
|
serverIo.on('connection', (socket) => {
|
||||||
serverSockets[socket.id] = setInterval(async () => {
|
// 存储对应websocket连接的定时器
|
||||||
try {
|
serverSockets[socket.id] = setInterval(async () => {
|
||||||
osData = await getOsData()
|
try {
|
||||||
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
osData = await getOsData()
|
||||||
} catch (error) {
|
socket && socket.emit('client_data', Object.assign(osData, { ipInfo }))
|
||||||
console.error('客户端错误:', error)
|
} catch (error) {
|
||||||
socket && socket.emit('client_error', { error })
|
consola.error('客户端错误:', error)
|
||||||
}
|
socket && socket.emit('client_error', { error })
|
||||||
}, 1000)
|
}
|
||||||
|
}, 1000)
|
||||||
socket.on('disconnect', () => {
|
|
||||||
// 断开时清楚对应的websocket连接
|
socket.on('disconnect', () => {
|
||||||
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
// 断开时清楚对应的websocket连接
|
||||||
delete serverSockets[socket.id]
|
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
||||||
socket.close && socket.close()
|
delete serverSockets[socket.id]
|
||||||
socket = null
|
socket.close && socket.close()
|
||||||
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
|
socket = null
|
||||||
})
|
// console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length)
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
256
server/app/socket/sftp.js
Normal file
256
server/app/socket/sftp.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
const { Server } = require('socket.io')
|
||||||
|
const SFTPClient = require('ssh2-sftp-client')
|
||||||
|
const rawPath = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
||||||
|
const { sftpCacheDir } = require('../config')
|
||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
|
||||||
|
function clearDir(path, rmSelf = false) {
|
||||||
|
let files = []
|
||||||
|
if(!fs.existsSync(path)) return consola.info('clearDir: 目标文件夹不存在')
|
||||||
|
files = fs.readdirSync(path)
|
||||||
|
files.forEach((file) => {
|
||||||
|
let curPath = path + '/' + file
|
||||||
|
if(fs.statSync(curPath).isDirectory()){
|
||||||
|
clearDir(curPath) //递归删除文件夹
|
||||||
|
fs.rmdirSync(curPath) // 删除文件夹
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(curPath) //删除文件
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if(rmSelf) fs.rmdirSync(path)
|
||||||
|
consola.success('clearDir: 已清空缓存文件')
|
||||||
|
}
|
||||||
|
const pipeStream = (path, writeStream) => {
|
||||||
|
// console.log('path', path)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const readStream = fs.createReadStream(path)
|
||||||
|
readStream.on('end', () => {
|
||||||
|
fs.unlinkSync(path) // 删除已写入切片
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
readStream.pipe(writeStream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenInput(sftpClient, socket) {
|
||||||
|
socket.on('open_dir', async (path) => {
|
||||||
|
const exists = await sftpClient.exists(path)
|
||||||
|
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
||||||
|
try {
|
||||||
|
let dirLs = await sftpClient.list(path)
|
||||||
|
socket.emit('dir_ls', dirLs)
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('open_dir Error', error.message)
|
||||||
|
socket.emit('sftp_error', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.on('rm_dir', async (path) => {
|
||||||
|
const exists = await sftpClient.exists(path)
|
||||||
|
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
||||||
|
try {
|
||||||
|
let res = await sftpClient.rmdir(path, true) // 递归删除
|
||||||
|
socket.emit('rm_success', res)
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('rm_dir Error', error.message)
|
||||||
|
socket.emit('sftp_error', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.on('rm_file', async (path) => {
|
||||||
|
const exists = await sftpClient.exists(path)
|
||||||
|
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
||||||
|
try {
|
||||||
|
let res = await sftpClient.delete(path)
|
||||||
|
socket.emit('rm_success', res)
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('rm_file Error', error.message)
|
||||||
|
socket.emit('sftp_error', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// socket.on('down_dir', async (path) => {
|
||||||
|
// const exists = await sftpClient.exists(path)
|
||||||
|
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
|
// let res = await sftpClient.downloadDir(path, sftpCacheDir)
|
||||||
|
// socket.emit('down_dir_success', res)
|
||||||
|
// })
|
||||||
|
socket.on('down_file', async ({ path, name, size, target = 'down' }) => {
|
||||||
|
// target: down or preview
|
||||||
|
const exists = await sftpClient.exists(path)
|
||||||
|
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
||||||
|
try {
|
||||||
|
const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
|
let timer = null
|
||||||
|
let res = await sftpClient.fastGet(path, localPath, {
|
||||||
|
step: step => {
|
||||||
|
if(timer) return
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2
|
||||||
|
console.log(`从服务器下载进度:${ percent }%`)
|
||||||
|
socket.emit('down_file_progress', percent)
|
||||||
|
timer = null
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
consola.success('sftp下载成功: ', res)
|
||||||
|
let buffer = fs.readFileSync(localPath)
|
||||||
|
let data = { buffer, name }
|
||||||
|
switch(target) {
|
||||||
|
case 'down':
|
||||||
|
socket.emit('down_file_success', data)
|
||||||
|
break
|
||||||
|
case 'preview':
|
||||||
|
socket.emit('preview_file_success', data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fs.unlinkSync(localPath) //删除文件
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('down_file Error', error.message)
|
||||||
|
socket.emit('sftp_error', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
|
||||||
|
// console.log({ targetPath, fullPath, name, file })
|
||||||
|
// const exists = await sftpClient.exists(targetPath)
|
||||||
|
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
|
// try {
|
||||||
|
// const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
|
// fs.writeFileSync(localPath, file)
|
||||||
|
// let res = await sftpClient.fastPut(localPath, fullPath)
|
||||||
|
// consola.success('sftp上传成功: ', res)
|
||||||
|
// socket.emit('up_file_success', res)
|
||||||
|
// } catch (error) {
|
||||||
|
// consola.error('up_file Error', error.message)
|
||||||
|
// socket.emit('sftp_error', error.message)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
/** 分片上传 */
|
||||||
|
// 1. 创建本地缓存文件夹
|
||||||
|
let md5List = []
|
||||||
|
socket.on('create_cache_dir', async ({ targetPath, name }) => {
|
||||||
|
// console.log({ targetPath, name })
|
||||||
|
const exists = await sftpClient.exists(targetPath)
|
||||||
|
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
|
md5List = []
|
||||||
|
const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
|
if(fs.existsSync(localPath)) clearDir(localPath) // 清空目录
|
||||||
|
fs.mkdirSync(localPath, { recursive: true })
|
||||||
|
console.log('================create_cache_success================')
|
||||||
|
socket.emit('create_cache_success')
|
||||||
|
})
|
||||||
|
socket.on('up_file_slice', async ({ name, sliceFile, fileIndex }) => {
|
||||||
|
// console.log('up_file_slice:', fileIndex, name)
|
||||||
|
try {
|
||||||
|
let md5 = `${ fileIndex }.${ CryptoJS.MD5(fileIndex+name).toString() }`
|
||||||
|
const localPath = rawPath.join(sftpCacheDir, name, md5)
|
||||||
|
md5List.push(localPath)
|
||||||
|
fs.writeFileSync(localPath, sliceFile)
|
||||||
|
socket.emit('up_file_slice_success', md5)
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('up_file_slice Error', error.message)
|
||||||
|
socket.emit('up_file_slice_fail', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.on('up_file_slice_over', async ({ name, fullPath, range, size }) => {
|
||||||
|
const resultDirPath = rawPath.join(sftpCacheDir, name)
|
||||||
|
const resultFilePath = rawPath.join(sftpCacheDir, name, name)
|
||||||
|
try {
|
||||||
|
console.log('md5List: ', md5List)
|
||||||
|
const arr = md5List.map((chunkFilePath, index) => {
|
||||||
|
return pipeStream(
|
||||||
|
chunkFilePath,
|
||||||
|
// 指定位置创建可写流
|
||||||
|
fs.createWriteStream(resultFilePath, {
|
||||||
|
start: index * range,
|
||||||
|
end: (index + 1) * range
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
md5List = []
|
||||||
|
await Promise.all(arr)
|
||||||
|
let timer = null
|
||||||
|
let res = await sftpClient.fastPut(resultFilePath, fullPath, {
|
||||||
|
step: step => {
|
||||||
|
if(timer) return
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
const percent = Math.ceil((step / size) * 100)
|
||||||
|
console.log(`上传服务器进度:${ percent }%`)
|
||||||
|
socket.emit('up_file_progress', percent)
|
||||||
|
timer = null
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
consola.success('sftp上传成功: ', res)
|
||||||
|
socket.emit('up_file_success', res)
|
||||||
|
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('sftp上传失败: ', error.message)
|
||||||
|
socket.emit('sftp_error', error.message)
|
||||||
|
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (httpServer) => {
|
||||||
|
const serverIo = new Server(httpServer, {
|
||||||
|
path: '/sftp',
|
||||||
|
cors: {
|
||||||
|
origin: '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
serverIo.on('connection', (socket) => {
|
||||||
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
|
let sftpClient = new SFTPClient()
|
||||||
|
consola.success('terminal websocket 已连接')
|
||||||
|
|
||||||
|
socket.on('create', ({ host: ip, token }) => {
|
||||||
|
const { code } = verifyAuth(token, clientIp)
|
||||||
|
if(code !== 1) {
|
||||||
|
socket.emit('token_verify_fail')
|
||||||
|
socket.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sshRecord = readSSHRecord()
|
||||||
|
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||||
|
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||||
|
let { type, host, port, username, randomKey } = loginInfo
|
||||||
|
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
||||||
|
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
||||||
|
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
||||||
|
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||||
|
consola.info('准备连接Sftp:', host)
|
||||||
|
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
||||||
|
sftpClient.connect(authInfo)
|
||||||
|
.then(() => {
|
||||||
|
consola.success('连接Sftp成功:', host)
|
||||||
|
return sftpClient.list('/')
|
||||||
|
})
|
||||||
|
.then((rootLs) => {
|
||||||
|
// 普通文件-、目录文件d、链接文件l
|
||||||
|
socket.emit('root_ls', rootLs) // 先返回根目录
|
||||||
|
listenInput(sftpClient, socket) // 监听前端请求
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
consola.error('创建Sftp失败:', err.message)
|
||||||
|
socket.emit('create_fail', err.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('disconnect', async () => {
|
||||||
|
sftpClient.end()
|
||||||
|
.then(() => {
|
||||||
|
consola.info('sftp连接断开')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
consola.info('sftp断开连接失败:', error.message)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sftpClient = null
|
||||||
|
const cacheDir = rawPath.join(sftpCacheDir)
|
||||||
|
clearDir(cacheDir)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,25 +1,29 @@
|
|||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const { Client: Client } = require('ssh2')
|
const { Client: SSHClient } = require('ssh2')
|
||||||
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
||||||
|
|
||||||
function createTerminal(socket, vps) {
|
function createTerminal(socket, sshClient) {
|
||||||
vps.shell({ term: 'xterm-color' }, (err, stream) => {
|
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||||
if (err) return socket.emit('output', err.toString())
|
if (err) return socket.emit('output', err.toString())
|
||||||
|
// 终端输出
|
||||||
stream
|
stream
|
||||||
.on('data', (data) => {
|
.on('data', (data) => {
|
||||||
socket.emit('output', data.toString())
|
socket.emit('output', data.toString())
|
||||||
})
|
})
|
||||||
.on('close', () => {
|
.on('close', () => {
|
||||||
console.log('关闭终端')
|
consola.info('关闭终端')
|
||||||
vps.end()
|
sshClient.end()
|
||||||
})
|
})
|
||||||
|
// web端输入
|
||||||
socket.on('input', key => {
|
socket.on('input', key => {
|
||||||
if(vps._sock.writable === false) return console.log('终端连接已关闭')
|
if(sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
||||||
stream.write(key)
|
stream.write(key)
|
||||||
})
|
})
|
||||||
socket.emit('connect_terminal')
|
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
||||||
|
|
||||||
|
// 监听按键重置终端大小
|
||||||
socket.on('resize', ({ rows, cols }) => {
|
socket.on('resize', ({ rows, cols }) => {
|
||||||
|
consola.info('更改tty终端行&列: ', { rows, cols })
|
||||||
stream.setWindow(rows, cols)
|
stream.setWindow(rows, cols)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -29,14 +33,14 @@ module.exports = (httpServer) => {
|
|||||||
const serverIo = new Server(httpServer, {
|
const serverIo = new Server(httpServer, {
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*'
|
origin: '*' // 'http://localhost:8080'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
let vps = new Client()
|
let sshClient = new SSHClient()
|
||||||
console.log('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
|
|
||||||
socket.on('create', ({ host: ip, token }) => {
|
socket.on('create', ({ host: ip, token }) => {
|
||||||
const { code } = verifyAuth(token, clientIp)
|
const { code } = verifyAuth(token, clientIp)
|
||||||
@ -45,7 +49,6 @@ module.exports = (httpServer) => {
|
|||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// console.log('code:', code)
|
|
||||||
const sshRecord = readSSHRecord()
|
const sshRecord = readSSHRecord()
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||||
@ -55,38 +58,30 @@ module.exports = (httpServer) => {
|
|||||||
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
||||||
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
||||||
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||||
console.log('准备连接服务器:', host)
|
consola.info('准备连接终端:', host)
|
||||||
vps
|
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
||||||
|
sshClient
|
||||||
.on('ready', () => {
|
.on('ready', () => {
|
||||||
console.log('已连接到服务器:', host)
|
consola.success('已连接到终端:', host)
|
||||||
socket.emit('connect_success', `已连接到服务器:${ host }`)
|
socket.emit('connect_success', `已连接到终端:${ host }`)
|
||||||
createTerminal(socket, vps)
|
createTerminal(socket, sshClient)
|
||||||
})
|
})
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
console.log('连接失败:', err.level)
|
consola.error('连接终端失败:', err.level)
|
||||||
socket.emit('connect_fail', err.message)
|
socket.emit('connect_fail', err.message)
|
||||||
})
|
})
|
||||||
.connect({
|
.connect(authInfo)
|
||||||
type: 'privateKey',
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username,
|
|
||||||
[type]: loginInfo[type]
|
|
||||||
// debug: (info) => {
|
|
||||||
// console.log(info)
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('创建失败:', err.message)
|
consola.error('创建终端失败:', err.message)
|
||||||
socket.emit('create_fail', err.message)
|
socket.emit('create_fail', err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
console.log('终端连接断开:', reason)
|
consola.info('终端连接断开:', reason)
|
||||||
vps.end()
|
sshClient.end()
|
||||||
vps.destroy()
|
sshClient.destroy()
|
||||||
vps = null
|
sshClient = null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
BIN
server/app/static/assets/delete.41fc4989.png
Normal file
BIN
server/app/static/assets/delete.41fc4989.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
32
server/app/static/assets/index.a9194a35.css
Normal file
32
server/app/static/assets/index.a9194a35.css
Normal file
File diff suppressed because one or more lines are too long
BIN
server/app/static/assets/index.a9194a35.css.gz
Normal file
BIN
server/app/static/assets/index.a9194a35.css.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
134
server/app/static/assets/index.eb5f280e.js
Normal file
134
server/app/static/assets/index.eb5f280e.js
Normal file
File diff suppressed because one or more lines are too long
BIN
server/app/static/assets/index.eb5f280e.js.gz
Normal file
BIN
server/app/static/assets/index.eb5f280e.js.gz
Normal file
Binary file not shown.
BIN
server/app/static/assets/link.86235911.png
Normal file
BIN
server/app/static/assets/link.86235911.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
server/app/static/assets/refresh.edd046ad.png
Normal file
BIN
server/app/static/assets/refresh.edd046ad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
@ -1,15 +1,17 @@
|
|||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
|
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
|
||||||
<title>EasyNode</title>
|
<title>EasyNode</title>
|
||||||
<script src="//at.alicdn.com/t/font_3309550_eg9tjmfmiku.js"></script>
|
<script src="//at.alicdn.com/t/c/font_3309550_flid0kihu36.js"></script>
|
||||||
<script type="module" crossorigin src="/assets/index.c20c6c58.js"></script>
|
<script type="module" crossorigin src="/assets/index.eb5f280e.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index.d8a03066.css">
|
<link rel="stylesheet" href="/assets/index.a9194a35.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
0
server/app/static/upload/.gitkeep
Normal file
0
server/app/static/upload/.gitkeep
Normal file
38
server/app/storage/README.md
Normal file
38
server/app/storage/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# host-list.json
|
||||||
|
|
||||||
|
> 存储服务器基本信息
|
||||||
|
|
||||||
|
# key.json
|
||||||
|
|
||||||
|
> 用于加密的密钥相关
|
||||||
|
|
||||||
|
# ssh-record.json
|
||||||
|
|
||||||
|
> ssh密钥记录(加密存储)
|
||||||
|
|
||||||
|
# email.json
|
||||||
|
|
||||||
|
> 邮件配置
|
||||||
|
|
||||||
|
- port: 587 --> secure: false
|
||||||
|
```json
|
||||||
|
// Gmail调试不通过, 暂缓
|
||||||
|
{
|
||||||
|
"name": "Google邮箱",
|
||||||
|
"target": "google",
|
||||||
|
"host": "smtp.gmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"tls": {
|
||||||
|
"rejectUnauthorized": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# notify.json
|
||||||
|
|
||||||
|
> 通知配置
|
||||||
|
|
||||||
|
# group.json
|
||||||
|
|
||||||
|
> 服务器分组配置
|
36
server/app/storage/email.json
Normal file
36
server/app/storage/email.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"support": [
|
||||||
|
{
|
||||||
|
"name": "QQ邮箱",
|
||||||
|
"target": "qq",
|
||||||
|
"host": "smtp.qq.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"tls": {
|
||||||
|
"rejectUnauthorized": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "网易126",
|
||||||
|
"target": "wangyi126",
|
||||||
|
"host": "smtp.126.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"tls": {
|
||||||
|
"rejectUnauthorized": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "网易163",
|
||||||
|
"target": "wangyi163",
|
||||||
|
"host": "smtp.163.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"tls": {
|
||||||
|
"rejectUnauthorized": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
]
|
||||||
|
}
|
7
server/app/storage/group.json
Normal file
7
server/app/storage/group.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"name": "默认分组",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
2
server/app/storage/host-list.json
Normal file
2
server/app/storage/host-list.json
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[
|
||||||
|
]
|
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"pwd": "admin",
|
"pwd": "admin",
|
||||||
"jwtExpires": "1h",
|
|
||||||
"commonKey": "",
|
"commonKey": "",
|
||||||
"publicKey": "",
|
"publicKey": "",
|
||||||
"privateKey": ""
|
"privateKey": ""
|
22
server/app/storage/notify.json
Normal file
22
server/app/storage/notify.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "login",
|
||||||
|
"desc": "登录面板提醒",
|
||||||
|
"sw": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "err_login",
|
||||||
|
"desc": "登录错误提醒(连续5次)",
|
||||||
|
"sw": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "updatePwd",
|
||||||
|
"desc": "修改密码提醒",
|
||||||
|
"sw": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "host_offline",
|
||||||
|
"desc": "客户端离线提醒(每小时最多发送一次提醒)",
|
||||||
|
"sw": true
|
||||||
|
}
|
||||||
|
]
|
23
server/app/template/commonTemp.html
Normal file
23
server/app/template/commonTemp.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="margin: 0; padding: 0;text-align: center;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h3 style="font-size: 20px;color: #5992D3;padding:0 0 0 40px;">
|
||||||
|
${ content }
|
||||||
|
</h3>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
26
server/app/template/commonTemp.js
Normal file
26
server/app/template/commonTemp.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module.exports = (content) => {
|
||||||
|
return `<!DOCTYPE html
|
||||||
|
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="margin: 0; padding: 0;text-align: center;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h3 style="font-size: 18px;color: #5992D3;padding:0 0 0 10px;">
|
||||||
|
${ content }
|
||||||
|
</h3>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
60
server/app/utils/email.js
Normal file
60
server/app/utils/email.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const nodemailer = require('nodemailer')
|
||||||
|
const { readSupportEmailList, readUserEmailList } = require('./storage')
|
||||||
|
const commonTemp = require('../template/commonTemp')
|
||||||
|
|
||||||
|
const emailCode = {
|
||||||
|
SUCCESS: 0,
|
||||||
|
FAIL: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailTransporter = async (params = {}) => {
|
||||||
|
let { toEmail, title, html } = params
|
||||||
|
try {
|
||||||
|
if(!toEmail) throw Error('missing params: toEmail')
|
||||||
|
let userEmail = readUserEmailList().find(({ auth }) => auth.user === toEmail)
|
||||||
|
if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`)
|
||||||
|
let { target } = userEmail
|
||||||
|
let emailServerConf = readSupportEmailList().find((item) => item.target === target)
|
||||||
|
if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`)
|
||||||
|
const timeout = 1000*6
|
||||||
|
let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout })
|
||||||
|
let transporter = nodemailer.createTransport(options)
|
||||||
|
let info = await transporter.sendMail({
|
||||||
|
from: userEmail.auth.user, // sender address
|
||||||
|
to: userEmail.auth.user, // list of receivers
|
||||||
|
subject: `EasyNode: ${ title }`,
|
||||||
|
html
|
||||||
|
})
|
||||||
|
// consola.success('email发送成功:', info.accepted)
|
||||||
|
return { code: emailCode.SUCCESS, msg: `send successful:${ info.accepted }` }
|
||||||
|
} catch(error) {
|
||||||
|
// consola.error(`email发送失败(${ toEmail }):`, error.message || error)
|
||||||
|
return { code: emailCode.FAIL, msg: error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmailToConfList = (title, content) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
|
let emailList = readUserEmailList()
|
||||||
|
if(Array.isArray(emailList) && emailList.length >= 1) {
|
||||||
|
for (const item of emailList) {
|
||||||
|
const toEmail = item.auth.user
|
||||||
|
await emailTransporter({ toEmail, title, html: commonTemp(content) })
|
||||||
|
.then(({ code }) => {
|
||||||
|
if(code === 0) {
|
||||||
|
consola.success('已发送邮件通知: ', toEmail, title)
|
||||||
|
return res({ code: emailCode.SUCCESS })
|
||||||
|
}
|
||||||
|
consola.error('邮件通知发送失败: ', toEmail, title)
|
||||||
|
return rej({ code: emailCode.FAIL })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
emailTransporter,
|
||||||
|
sendEmailToConfList
|
||||||
|
}
|
44
server/app/utils/encrypt.js
Normal file
44
server/app/utils/encrypt.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
const rawCrypto = require('crypto')
|
||||||
|
const NodeRSA = require('node-rsa')
|
||||||
|
const { readKey } = require('./storage.js')
|
||||||
|
|
||||||
|
// rsa非对称 私钥解密
|
||||||
|
const RSADecrypt = (ciphertext) => {
|
||||||
|
if(!ciphertext) return
|
||||||
|
let { privateKey } = readKey()
|
||||||
|
privateKey = AESDecrypt(privateKey) // 先解密私钥
|
||||||
|
const rsakey = new NodeRSA(privateKey)
|
||||||
|
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
|
||||||
|
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
||||||
|
return plaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
// aes对称 加密(default commonKey)
|
||||||
|
const AESEncrypt = (text, key) => {
|
||||||
|
if(!text) return
|
||||||
|
let { commonKey } = readKey()
|
||||||
|
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
||||||
|
return ciphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
// aes对称 解密(default commonKey)
|
||||||
|
const AESDecrypt = (ciphertext, key) => {
|
||||||
|
if(!ciphertext) return
|
||||||
|
let { commonKey } = readKey()
|
||||||
|
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
||||||
|
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
||||||
|
return originalText
|
||||||
|
}
|
||||||
|
|
||||||
|
// sha1 加密(不可逆)
|
||||||
|
const SHA1Encrypt = (clearText) => {
|
||||||
|
return rawCrypto.createHash('sha1').update(clearText).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RSADecrypt,
|
||||||
|
AESEncrypt,
|
||||||
|
AESDecrypt,
|
||||||
|
SHA1Encrypt
|
||||||
|
}
|
@ -1,189 +1,50 @@
|
|||||||
const fs = require('fs')
|
const {
|
||||||
const CryptoJS = require('crypto-js')
|
|
||||||
const rawCrypto = require('crypto')
|
|
||||||
const NodeRSA = require('node-rsa')
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const axios = require('axios')
|
|
||||||
const request = axios.create({ timeout: 3000 })
|
|
||||||
|
|
||||||
const { sshRecordPath, hostListPath, keyPath } = require('../config')
|
|
||||||
|
|
||||||
const readSSHRecord = () => {
|
|
||||||
let list
|
|
||||||
try {
|
|
||||||
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
|
||||||
} catch (error) {
|
|
||||||
console.log('读取ssh-record错误, 即将重置ssh列表: ', 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) {
|
|
||||||
console.log('读取host-list错误, 即将重置host列表: ', 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为空时请求本地IP
|
|
||||||
const getNetIPInfo = async (ip = '') => {
|
|
||||||
try {
|
|
||||||
let date = getUTCDate(8)
|
|
||||||
let ipUrls = [`http://ip-api.com/json/${ ip }?lang=zh-CN`, `http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=${ ip }`]
|
|
||||||
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
|
|
||||||
let [ipApi, pconline] = result
|
|
||||||
if(ipApi.status === 'fulfilled') {
|
|
||||||
let { query: ip, country, regionName, city } = ipApi.value.data
|
|
||||||
// console.log({ ip, country, city: regionName + city })
|
|
||||||
return { ip, country, city: regionName + city, date }
|
|
||||||
}
|
|
||||||
if(pconline.status === 'fulfilled') {
|
|
||||||
let { ip, pro, city, addr } = pconline.value.data
|
|
||||||
// console.log({ ip, country: pro || addr, city })
|
|
||||||
return { ip, country: pro || addr, city, date }
|
|
||||||
}
|
|
||||||
throw Error('获取IP信息API出错,请排查或更新API')
|
|
||||||
} 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 || 16
|
|
||||||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
|
||||||
a = str.length,
|
|
||||||
res = ''
|
|
||||||
for (let i = 0; i < e; i++) res += str.charAt(Math.floor(Math.random() * a))
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验token与登录IP
|
|
||||||
const verifyAuth = (token, clientIp) =>{
|
|
||||||
token = AESDecrypt(token) // 先aes解密
|
|
||||||
const { commonKey } = readKey()
|
|
||||||
try {
|
|
||||||
const { exp } = jwt.verify(token, commonKey)
|
|
||||||
// console.log('校验token:', new Date(), '---', new Date(exp * 1000))
|
|
||||||
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
|
||||||
|
|
||||||
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
|
|
||||||
console.log('校验客户端IP:', clientIp)
|
|
||||||
console.log('最后登录的IP:', lastLoginIp)
|
|
||||||
// 判断: (生产环境)clientIp与上次登录成功IP不一致
|
|
||||||
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
|
|
||||||
return { code: -1, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
|
|
||||||
}
|
|
||||||
// console.log('token验证成功')
|
|
||||||
return { code: 1, msg: 'success' } // 验证成功
|
|
||||||
} catch (error) {
|
|
||||||
// console.log('token校验错误:', error)
|
|
||||||
return { code: -2, msg: error } // token错误, 验证失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProd = () => {
|
|
||||||
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
|
||||||
return EXEC_ENV === 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsa非对称 私钥解密
|
|
||||||
const RSADecrypt = (ciphertext) => {
|
|
||||||
if(!ciphertext) return
|
|
||||||
let { privateKey } = readKey()
|
|
||||||
privateKey = AESDecrypt(privateKey) // 先解密私钥
|
|
||||||
const rsakey = new NodeRSA(privateKey)
|
|
||||||
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
|
|
||||||
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
|
||||||
return plaintext
|
|
||||||
}
|
|
||||||
|
|
||||||
// aes对称 加密(default commonKey)
|
|
||||||
const AESEncrypt = (text, key) => {
|
|
||||||
if(!text) return
|
|
||||||
let { commonKey } = readKey()
|
|
||||||
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
|
||||||
return ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
// aes对称 解密(default commonKey)
|
|
||||||
const AESDecrypt = (ciphertext, key) => {
|
|
||||||
if(!ciphertext) return
|
|
||||||
let { commonKey } = readKey()
|
|
||||||
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
|
||||||
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
|
||||||
return originalText
|
|
||||||
}
|
|
||||||
|
|
||||||
// sha1 加密(不可逆)
|
|
||||||
const SHA1Encrypt = (clearText) => {
|
|
||||||
return rawCrypto.createHash('sha1').update(clearText).digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取UTC-x时间
|
|
||||||
const getUTCDate = (num = 8) => {
|
|
||||||
let date = new Date()
|
|
||||||
let now_utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(),
|
|
||||||
date.getUTCDate(), date.getUTCHours() + num,
|
|
||||||
date.getUTCMinutes(), date.getUTCSeconds())
|
|
||||||
return new Date(now_utc)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
readSSHRecord,
|
readSSHRecord,
|
||||||
writeSSHRecord,
|
writeSSHRecord,
|
||||||
readHostList,
|
readHostList,
|
||||||
writeHostList,
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList } = require('./storage')
|
||||||
|
const { RSADecrypt, AESEncrypt, AESDecrypt, SHA1Encrypt } = require('./encrypt')
|
||||||
|
const { verifyAuth, isProd } = require('./verify-auth')
|
||||||
|
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
|
||||||
|
const { emailTransporter, sendEmailToConfList } = require('./email')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
getNetIPInfo,
|
getNetIPInfo,
|
||||||
throwError,
|
throwError,
|
||||||
isIP,
|
isIP,
|
||||||
readKey,
|
|
||||||
writeKey,
|
|
||||||
randomStr,
|
randomStr,
|
||||||
|
getUTCDate,
|
||||||
|
formatTimestamp,
|
||||||
verifyAuth,
|
verifyAuth,
|
||||||
isProd,
|
isProd,
|
||||||
RSADecrypt,
|
RSADecrypt,
|
||||||
AESEncrypt,
|
AESEncrypt,
|
||||||
AESDecrypt,
|
AESDecrypt,
|
||||||
SHA1Encrypt,
|
SHA1Encrypt,
|
||||||
getUTCDate
|
readSSHRecord,
|
||||||
}
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
emailTransporter,
|
||||||
|
sendEmailToConfList,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList
|
||||||
|
}
|
||||||
|
139
server/app/utils/storage.js
Normal file
139
server/app/utils/storage.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const { sshRecordPath, hostListPath, keyPath, emailPath, notifyPath, groupPath } = require('../config')
|
||||||
|
|
||||||
|
const readSSHRecord = () => {
|
||||||
|
let list
|
||||||
|
try {
|
||||||
|
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取ssh-record错误, 即将重置ssh列表: ', 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) {
|
||||||
|
consola.error('读取host-list错误, 即将重置host列表: ', 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 readEmailJson = () => {
|
||||||
|
let emailJson = {}
|
||||||
|
try {
|
||||||
|
emailJson = JSON.parse(fs.readFileSync(emailPath, 'utf8'))
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取email.json错误: ', error)
|
||||||
|
}
|
||||||
|
return emailJson
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSupportEmailList = () => {
|
||||||
|
let supportEmailList = []
|
||||||
|
try {
|
||||||
|
supportEmailList = readEmailJson().support
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取email support错误: ', error)
|
||||||
|
}
|
||||||
|
return supportEmailList
|
||||||
|
}
|
||||||
|
|
||||||
|
const readUserEmailList = () => {
|
||||||
|
let configEmailList = []
|
||||||
|
try {
|
||||||
|
configEmailList = readEmailJson().user
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取email config错误: ', error)
|
||||||
|
}
|
||||||
|
return configEmailList
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeUserEmailList = (user) => {
|
||||||
|
let support = readSupportEmailList()
|
||||||
|
const emailJson = { support, user }
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(emailPath, JSON.stringify(emailJson, null, 2))
|
||||||
|
return { code: 0 }
|
||||||
|
} catch (error) {
|
||||||
|
return { code: -1, msg: error.message || error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readNotifyList = () => {
|
||||||
|
let notifyList = []
|
||||||
|
try {
|
||||||
|
notifyList = JSON.parse(fs.readFileSync(notifyPath, 'utf8'))
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取notify list错误: ', error)
|
||||||
|
}
|
||||||
|
return notifyList
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNotifySwByType = (type) => {
|
||||||
|
if(!type) throw Error('missing params: type')
|
||||||
|
try {
|
||||||
|
let { sw } = readNotifyList().find((item) => item.type === type)
|
||||||
|
return sw
|
||||||
|
} catch (error) {
|
||||||
|
consola.error(`通知类型[${ type }]不存在`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeNotifyList = (notifyList) => {
|
||||||
|
fs.writeFileSync(notifyPath, JSON.stringify(notifyList, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const readGroupList = () => {
|
||||||
|
let list
|
||||||
|
try {
|
||||||
|
list = JSON.parse(fs.readFileSync(groupPath, 'utf8'))
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取group-list错误, 即将重置group列表: ', error)
|
||||||
|
writeSSHRecord([])
|
||||||
|
}
|
||||||
|
return list || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeGroupList = (list = []) => {
|
||||||
|
fs.writeFileSync(groupPath, JSON.stringify(list, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
readSSHRecord,
|
||||||
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList
|
||||||
|
}
|
47
server/app/utils/test-connect.js
Normal file
47
server/app/utils/test-connect.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// based off of https://github.com/apaszke/tcp-ping
|
||||||
|
// rewritten with modern es6 syntax & promises
|
||||||
|
const { io: ClientIO } = require('socket.io-client')
|
||||||
|
|
||||||
|
const testConnectAsync = (options) => {
|
||||||
|
let connectTimes = 0
|
||||||
|
options = Object.assign({ retryTimes: 3, timeout: 5000, host: 'http://localhost', port: '80' }, options)
|
||||||
|
const { retryTimes, host, port, timeout } = options
|
||||||
|
// eslint-disable-next-line
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
while (connectTimes < retryTimes) {
|
||||||
|
try {
|
||||||
|
connectTimes++
|
||||||
|
await connect({ host, port, timeout })
|
||||||
|
break
|
||||||
|
} catch (error) {
|
||||||
|
// 重连次数达到限制仍未连接成功
|
||||||
|
if(connectTimes === retryTimes) {
|
||||||
|
reject({ message: error.message, host, port, connectTimes })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve({ status: 'connect_success', host, port, connectTimes })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const connect = (options) => {
|
||||||
|
const { host, port, timeout } = options
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let io = ClientIO(`${ host }:${ port }`, {
|
||||||
|
path: '/client/os-info',
|
||||||
|
forceNew: false,
|
||||||
|
timeout,
|
||||||
|
reconnection: false
|
||||||
|
})
|
||||||
|
.on('connect', () => {
|
||||||
|
resolve()
|
||||||
|
io.disconnect()
|
||||||
|
})
|
||||||
|
.on('connect_error', (error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = testConnectAsync
|
144
server/app/utils/tools.js
Normal file
144
server/app/utils/tools.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
const axios = require('axios')
|
||||||
|
const request = axios.create({ timeout: 3000 })
|
||||||
|
|
||||||
|
// 为空时请求本地IP
|
||||||
|
const getNetIPInfo = async (searchIp = '') => {
|
||||||
|
searchIp = searchIp.replace(/::ffff:/g, '') || '' // fix: nginx反代
|
||||||
|
if(['::ffff:', '::1'].includes(searchIp)) searchIp = '127.0.0.1'
|
||||||
|
try {
|
||||||
|
let date = Date.now()
|
||||||
|
let ipUrls = [
|
||||||
|
// 45次/分钟&支持中文(无限制)
|
||||||
|
`http://ip-api.com/json/${ searchIp }?lang=zh-CN`,
|
||||||
|
// 10000次/月&支持中文(依赖IP计算调用次数)
|
||||||
|
`http://ipwho.is/${ searchIp }?lang=zh-CN`,
|
||||||
|
// 1500次/天(依赖密钥, 超出自行注册)
|
||||||
|
`https://api.ipdata.co/${ searchIp }?api-key=c6d4d04d5f11f2cd0839ee03c47c58621d74e361c945b5c1b4f668f3`,
|
||||||
|
// 50000/月(依赖密钥, 超出自行注册)
|
||||||
|
`https://ipinfo.io/${ searchIp }/json?token=41c48b54f6d78f`,
|
||||||
|
// 1000次/天(依赖密钥, 超出自行注册)
|
||||||
|
`https://api.ipgeolocation.io/ipgeo?apiKey=105fc2c7e8864ec08b98e1ad4e8cbc6d&ip=${ searchIp }`,
|
||||||
|
// 1000次/天(依赖IP计算调用次数)
|
||||||
|
`https://ipapi.co${ searchIp ? `/${ searchIp }` : '' }/json`,
|
||||||
|
// 国内IP138提供(无限制)
|
||||||
|
`https://sp1.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=${ searchIp }&resource_id=5809`
|
||||||
|
]
|
||||||
|
let result = await Promise.allSettled(ipUrls.map(url => request.get(url)))
|
||||||
|
|
||||||
|
let [ipApi, ipwho, ipdata, ipinfo, ipgeolocation, ipApi01, ip138] = result
|
||||||
|
|
||||||
|
let searchResult = []
|
||||||
|
if(ipApi.status === 'fulfilled') {
|
||||||
|
let { query: ip, country, regionName, city } = ipApi.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ipwho.status === 'fulfilled') {
|
||||||
|
let { ip, country, region: regionName, city } = ipwho.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ipdata.status === 'fulfilled') {
|
||||||
|
let { ip, country_name: country, region: regionName, city } = ipdata.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ipinfo.status === 'fulfilled') {
|
||||||
|
let { ip, country, region: regionName, city } = ipinfo.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ipgeolocation.status === 'fulfilled') {
|
||||||
|
let { ip, country_name: country, state_prov: regionName, city } = ipgeolocation.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ipApi01.status === 'fulfilled') {
|
||||||
|
let { ip, country_name: country, region: regionName, city } = ipApi01.value?.data || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ip138.status === 'fulfilled') {
|
||||||
|
let [res] = ip138.value?.data?.data || []
|
||||||
|
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
||||||
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
|
}
|
||||||
|
console.log(searchResult)
|
||||||
|
let validInfo = searchResult.find(item => Boolean(item.country))
|
||||||
|
consola.info('查询IP信息:', validInfo)
|
||||||
|
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
||||||
|
} catch (error) {
|
||||||
|
consola.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 = (len) =>{
|
||||||
|
len = len || 16
|
||||||
|
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
||||||
|
a = str.length,
|
||||||
|
res = ''
|
||||||
|
for (let i = 0; i < len; i++) res += str.charAt(Math.floor(Math.random() * a))
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取UTC-x时间
|
||||||
|
const getUTCDate = (num = 8) => {
|
||||||
|
let date = new Date()
|
||||||
|
let now_utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(),
|
||||||
|
date.getUTCDate(), date.getUTCHours() + num,
|
||||||
|
date.getUTCMinutes(), date.getUTCSeconds())
|
||||||
|
return new Date(now_utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
||||||
|
if(typeof(timestamp) !== 'number') return '--'
|
||||||
|
let date = new Date(timestamp)
|
||||||
|
let padZero = (num) => String(num).padStart(2, '0')
|
||||||
|
let year = date.getFullYear()
|
||||||
|
let mounth = padZero(date.getMonth() + 1)
|
||||||
|
let day = padZero(date.getDate())
|
||||||
|
let hours = padZero(date.getHours())
|
||||||
|
let minute = padZero(date.getMinutes())
|
||||||
|
let second = padZero(date.getSeconds())
|
||||||
|
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六' ]
|
||||||
|
let week = weekday[date.getDay()]
|
||||||
|
switch (format) {
|
||||||
|
case 'date':
|
||||||
|
return `${ year }-${ mounth }-${ day }`
|
||||||
|
case 'week':
|
||||||
|
return `${ year }-${ mounth }-${ day } ${ week }`
|
||||||
|
case 'hour':
|
||||||
|
return `${ year }-${ mounth }-${ day } ${ hours }`
|
||||||
|
case 'time':
|
||||||
|
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||||||
|
default:
|
||||||
|
return `${ year }-${ mounth }-${ day } ${ hours }:${ minute }:${ second }`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getNetIPInfo,
|
||||||
|
throwError,
|
||||||
|
isIP,
|
||||||
|
randomStr,
|
||||||
|
getUTCDate,
|
||||||
|
formatTimestamp
|
||||||
|
}
|
42
server/app/utils/verify-auth.js
Normal file
42
server/app/utils/verify-auth.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
const { AESDecrypt } = require('./encrypt')
|
||||||
|
const { readKey } = require('./storage')
|
||||||
|
const jwt = require('jsonwebtoken')
|
||||||
|
|
||||||
|
const enumLoginCode = {
|
||||||
|
SUCCESS: 1,
|
||||||
|
EXPIRES: -1,
|
||||||
|
ERROR_TOKEN: -2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验token与登录IP
|
||||||
|
const verifyAuth = (token, clientIp) =>{
|
||||||
|
if(['::ffff:', '::1'].includes(clientIp)) clientIp = '127.0.0.1'
|
||||||
|
token = AESDecrypt(token) // 先aes解密
|
||||||
|
const { commonKey } = readKey()
|
||||||
|
try {
|
||||||
|
const { exp } = jwt.verify(token, commonKey)
|
||||||
|
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
||||||
|
|
||||||
|
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
|
||||||
|
consola.info('校验客户端IP:', clientIp)
|
||||||
|
consola.info('最后登录的IP:', lastLoginIp)
|
||||||
|
// 判断: (生产环境)clientIp与上次登录成功IP不一致
|
||||||
|
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
|
||||||
|
return { code: enumLoginCode.EXPIRES, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
|
||||||
|
}
|
||||||
|
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
|
||||||
|
} catch (error) {
|
||||||
|
return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProd = () => {
|
||||||
|
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
||||||
|
return EXEC_ENV === 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyAuth,
|
||||||
|
isProd
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "easynode-server",
|
"name": "easynode-server",
|
||||||
"version": "1.1.0",
|
"version": "0.0.1",
|
||||||
"description": "easynode-server",
|
"description": "easynode-server",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@ -27,9 +27,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^3.1.0",
|
"@koa/cors": "^3.1.0",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
|
"consola": "^2.15.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"is-ip": "^4.0.0",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa": "^2.13.1",
|
"koa": "^2.13.1",
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^4.2.0",
|
||||||
@ -44,12 +45,13 @@
|
|||||||
"node-os-utils": "^1.3.6",
|
"node-os-utils": "^1.3.6",
|
||||||
"node-rsa": "^1.1.1",
|
"node-rsa": "^1.1.1",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
|
"nodemailer": "^6.7.5",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.4.1",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.5.1",
|
||||||
"ssh2": "^1.10.0"
|
"ssh2": "^1.10.0",
|
||||||
|
"ssh2-sftp-client": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
"pkg": "5.6"
|
"pkg": "5.6"
|
||||||
|
@ -358,6 +358,11 @@ buffer-equal-constant-time@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||||
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
||||||
|
|
||||||
|
buffer-from@^1.0.0:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||||
|
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||||
|
|
||||||
buffer@^5.5.0:
|
buffer@^5.5.0:
|
||||||
version "5.7.1"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||||
@ -544,6 +549,16 @@ concat-map@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||||
|
|
||||||
|
concat-stream@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
|
||||||
|
integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
|
||||||
|
dependencies:
|
||||||
|
buffer-from "^1.0.0"
|
||||||
|
inherits "^2.0.3"
|
||||||
|
readable-stream "^3.0.2"
|
||||||
|
typedarray "^0.0.6"
|
||||||
|
|
||||||
configstore@^5.0.1:
|
configstore@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
||||||
@ -556,6 +571,11 @@ configstore@^5.0.1:
|
|||||||
write-file-atomic "^3.0.0"
|
write-file-atomic "^3.0.0"
|
||||||
xdg-basedir "^4.0.0"
|
xdg-basedir "^4.0.0"
|
||||||
|
|
||||||
|
consola@^2.15.3:
|
||||||
|
version "2.15.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
|
||||||
|
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
|
||||||
|
|
||||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||||
@ -826,6 +846,11 @@ enquirer@^2.3.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-colors "^4.1.1"
|
ansi-colors "^4.1.1"
|
||||||
|
|
||||||
|
err-code@^2.0.2:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
|
||||||
|
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
|
||||||
|
|
||||||
escalade@^3.1.1:
|
escalade@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||||
@ -1421,11 +1446,6 @@ into-stream@^6.0.0:
|
|||||||
from2 "^2.3.0"
|
from2 "^2.3.0"
|
||||||
p-is-promise "^3.0.0"
|
p-is-promise "^3.0.0"
|
||||||
|
|
||||||
ip-regex@^5.0.0:
|
|
||||||
version "5.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632"
|
|
||||||
integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==
|
|
||||||
|
|
||||||
is-binary-path@~2.1.0:
|
is-binary-path@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||||
@ -1486,13 +1506,6 @@ is-installed-globally@^0.4.0:
|
|||||||
global-dirs "^3.0.0"
|
global-dirs "^3.0.0"
|
||||||
is-path-inside "^3.0.2"
|
is-path-inside "^3.0.2"
|
||||||
|
|
||||||
is-ip@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-4.0.0.tgz#8e9eae12056bf46edafad19054dcc3666a324b3a"
|
|
||||||
integrity sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==
|
|
||||||
dependencies:
|
|
||||||
ip-regex "^5.0.0"
|
|
||||||
|
|
||||||
is-nan@^1.3.2:
|
is-nan@^1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
|
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
|
||||||
@ -1954,6 +1967,11 @@ nan@^2.15.0:
|
|||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
||||||
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
||||||
|
|
||||||
|
nan@^2.16.0:
|
||||||
|
version "2.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
|
||||||
|
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
|
||||||
|
|
||||||
napi-build-utils@^1.0.1:
|
napi-build-utils@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
||||||
@ -2004,6 +2022,11 @@ node-schedule@^2.1.0:
|
|||||||
long-timeout "0.1.1"
|
long-timeout "0.1.1"
|
||||||
sorted-array-functions "^1.3.0"
|
sorted-array-functions "^1.3.0"
|
||||||
|
|
||||||
|
nodemailer@^6.7.5:
|
||||||
|
version "6.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.5.tgz#b30b1566f5fa2249f7bd49ced4c58bec6b25915e"
|
||||||
|
integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==
|
||||||
|
|
||||||
nodemon@^2.0.15:
|
nodemon@^2.0.15:
|
||||||
version "2.0.16"
|
version "2.0.16"
|
||||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef"
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef"
|
||||||
@ -2278,6 +2301,14 @@ progress@^2.0.0, progress@^2.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||||
|
|
||||||
|
promise-retry@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
|
||||||
|
integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
|
||||||
|
dependencies:
|
||||||
|
err-code "^2.0.2"
|
||||||
|
retry "^0.12.0"
|
||||||
|
|
||||||
pstree.remy@^1.1.8:
|
pstree.remy@^1.1.8:
|
||||||
version "1.1.8"
|
version "1.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
||||||
@ -2348,7 +2379,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4:
|
|||||||
string_decoder "~1.1.1"
|
string_decoder "~1.1.1"
|
||||||
util-deprecate "~1.0.1"
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||||
@ -2422,6 +2453,11 @@ responselike@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lowercase-keys "^1.0.0"
|
lowercase-keys "^1.0.0"
|
||||||
|
|
||||||
|
retry@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||||
|
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
|
||||||
|
|
||||||
reusify@^1.0.4:
|
reusify@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||||
@ -2613,6 +2649,15 @@ sprintf-js@~1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
|
ssh2-sftp-client@^9.0.1:
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ssh2-sftp-client/-/ssh2-sftp-client-9.0.1.tgz#0938ce12a8c07cf309de688028b0f97c7568bc0b"
|
||||||
|
integrity sha512-P8D7cDzSPJj3GKdTPSpK4rmPIJDFQagavaHax3KXgWciLoDM5czAGEU2OP4XlS5xDiIgHS1l6x9285Vs8kTxPA==
|
||||||
|
dependencies:
|
||||||
|
concat-stream "^2.0.0"
|
||||||
|
promise-retry "^2.0.1"
|
||||||
|
ssh2 "^1.11.0"
|
||||||
|
|
||||||
ssh2@^1.10.0:
|
ssh2@^1.10.0:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.10.0.tgz#e05d870dfc8e83bc918a2ffb3dcbd4d523472dee"
|
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.10.0.tgz#e05d870dfc8e83bc918a2ffb3dcbd4d523472dee"
|
||||||
@ -2624,6 +2669,17 @@ ssh2@^1.10.0:
|
|||||||
cpu-features "~0.0.4"
|
cpu-features "~0.0.4"
|
||||||
nan "^2.15.0"
|
nan "^2.15.0"
|
||||||
|
|
||||||
|
ssh2@^1.11.0:
|
||||||
|
version "1.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.11.0.tgz#ce60186216971e12f6deb553dcf82322498fe2e4"
|
||||||
|
integrity sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==
|
||||||
|
dependencies:
|
||||||
|
asn1 "^0.2.4"
|
||||||
|
bcrypt-pbkdf "^1.0.2"
|
||||||
|
optionalDependencies:
|
||||||
|
cpu-features "~0.0.4"
|
||||||
|
nan "^2.16.0"
|
||||||
|
|
||||||
statuses@2.0.1, statuses@^2.0.1:
|
statuses@2.0.1, statuses@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||||
@ -2725,11 +2781,6 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
systeminformation@^5.11.16:
|
|
||||||
version "5.11.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.16.tgz#5f6fda2447fafe204bd2ab543475f1ffa8c14a85"
|
|
||||||
integrity sha512-/a1VfP9WELKLT330yhAHJ4lWCXRYynel1kMMHKc/qdzCgDt3BIcMlo+3tKcTiRHFefjV3fz4AvqMx7dGO/72zw==
|
|
||||||
|
|
||||||
table@^6.0.9:
|
table@^6.0.9:
|
||||||
version "6.8.0"
|
version "6.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
|
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
|
||||||
@ -2862,6 +2913,11 @@ typedarray-to-buffer@^3.1.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-typedarray "^1.0.0"
|
is-typedarray "^1.0.0"
|
||||||
|
|
||||||
|
typedarray@^0.0.6:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||||
|
|
||||||
undefsafe@^2.0.5:
|
undefsafe@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user