Compare commits
No commits in common. "main" and "v2.0" have entirely different histories.
79
.github/workflows/client-builder.yml
vendored
@ -1,79 +0,0 @@
|
|||||||
name: Build Client to Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'client/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
working-directory: client
|
|
||||||
|
|
||||||
- name: Build for Linux x64
|
|
||||||
run: npm run pkglinux:x64
|
|
||||||
working-directory: client
|
|
||||||
|
|
||||||
- name: Install QEMU # 设置qemu 支持arm的虚拟环境
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y qemu qemu-user-static binfmt-support
|
|
||||||
|
|
||||||
- name: Setup QEMU ARM64
|
|
||||||
run: |
|
|
||||||
sudo update-binfmts --enable qemu-aarch64
|
|
||||||
sudo cp /usr/bin/qemu-aarch64-static /usr/local/bin
|
|
||||||
uname -a
|
|
||||||
|
|
||||||
- name: Build for Linux arm64
|
|
||||||
run: npm run pkglinux:arm64
|
|
||||||
working-directory: client
|
|
||||||
|
|
||||||
- name: Set tag name
|
|
||||||
id: tag_name
|
|
||||||
run: echo "TAG_NAME=client-$(date +'%Y-%m-%d')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
release_name: ${{ env.TAG_NAME }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
|
||||||
id: upload-release-asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
|
||||||
asset_path: client/dist/easynode-client-x64
|
|
||||||
asset_name: easynode-client-x64
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
||||||
|
|
||||||
- name: Upload Linux ARM64 Binary to Release
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: client/dist/easynode-client-arm64
|
|
||||||
asset_name: easynode-client-arm64
|
|
||||||
asset_content_type: application/octet-stream
|
|
40
.github/workflows/docker-builder.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: Build Server to DockerHub
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag_name:
|
|
||||||
description: 'Tag Name (leave empty for default latest)'
|
|
||||||
required: false
|
|
||||||
default: 'latest'
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out the repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: |
|
|
||||||
chaoszhu/easynode:${{ github.event.release.tag_name || inputs.tag_name }}
|
|
||||||
chaoszhu/easynode:latest
|
|
||||||
|
|
||||||
# - name: Clean up post-build
|
|
||||||
# run: docker system prune -af
|
|
4
.gitignore
vendored
@ -11,7 +11,3 @@ server/app/db/*
|
|||||||
plan.md
|
plan.md
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env-encrypt-key
|
|
||||||
*clear.js
|
|
||||||
local-script
|
|
||||||
版本发布.md
|
|
||||||
|
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"cSpell.ignoreWords": [
|
|
||||||
"Onekey"
|
|
||||||
]
|
|
||||||
}
|
|
202
CHANGELOG.md
@ -1,205 +1,3 @@
|
|||||||
## [3.0.3](https://github.com/chaos-zhu/easynode/releases) (2024-12-22)
|
|
||||||
|
|
||||||
* 支持keyboard-interactive服务器验证(serv00验证通过)
|
|
||||||
* 支持TG Bot通知方式
|
|
||||||
* 添加web端Plus授权功能
|
|
||||||
* 修复一些UI问题
|
|
||||||
* 修复MFA2登录验证码为0开头无法输入的bug
|
|
||||||
|
|
||||||
## [3.0.2](https://github.com/chaos-zhu/easynode/releases) (2024-11-20)
|
|
||||||
|
|
||||||
* 修复添加实例错误禁用的bug
|
|
||||||
|
|
||||||
|
|
||||||
## [3.0.1](https://github.com/chaos-zhu/easynode/releases) (2024-11-18)
|
|
||||||
|
|
||||||
* 修复同IP实例SFTP连接到其他的实例的bug
|
|
||||||
* 修复一些UI问题
|
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-11-09)
|
|
||||||
|
|
||||||
* 新增跳板机功能,支持选择多台机器跳转
|
|
||||||
* 脚本库批量导出导入
|
|
||||||
* 本地socket断开自动重连,无需手动重新连接
|
|
||||||
* 支持脚本库模糊搜索功能
|
|
||||||
* 分组添加实例数量标识
|
|
||||||
* 优化登录逻辑
|
|
||||||
* 默认登录有效期更改为当天有效
|
|
||||||
* 优化脚本库新增脚本时序号自动累加
|
|
||||||
* 修复一些小bug
|
|
||||||
|
|
||||||
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-24)
|
|
||||||
|
|
||||||
* 重构本地数据库存储方式(性能提升一个level~)
|
|
||||||
* 支持MFA2二次登录验证
|
|
||||||
* 优化了一些页面在移动端的展示
|
|
||||||
* 修复偶现刷新页面需重新登录的bug
|
|
||||||
|
|
||||||
|
|
||||||
## [2.2.8](https://github.com/chaos-zhu/easynode/releases) (2024-10-20)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 兼容移动端UI
|
|
||||||
* 新增移动端虚拟功能按键映射
|
|
||||||
* 调整终端功能菜单
|
|
||||||
* 登录日志本地化储存
|
|
||||||
* 修复终端选中文本无法复制的bug
|
|
||||||
* 修复无法展示服务端ping客户端延迟ms的bug
|
|
||||||
* 修复暗黑模式下的一些样式问题
|
|
||||||
|
|
||||||
## [2.2.7](https://github.com/chaos-zhu/easynode/releases) (2024-10-17)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 终端连接页新增展示服务端ping客户端延迟ms
|
|
||||||
* 修复自定义客户端端口默认字符串的bug
|
|
||||||
* 终端支持快捷设置开关: 快捷复制、快捷粘贴、选中脚本自动执行
|
|
||||||
|
|
||||||
## [2.2.6](https://github.com/chaos-zhu/easynode/releases) (2024-10-14)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 支持自定义客户端端口,方便穿透内网机器
|
|
||||||
* 修复监控数据意外注入bug
|
|
||||||
|
|
||||||
## [2.2.5](https://github.com/chaos-zhu/easynode/releases) (2024-10-11)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 不再对同IP:PORT的实例进行校验
|
|
||||||
* 支持同IP任意端口的服务器录入
|
|
||||||
* 支持关闭所有终端连接
|
|
||||||
* 修复第三方git代理地址
|
|
||||||
|
|
||||||
## [2.2.4](https://github.com/chaos-zhu/easynode/releases) (2024-08-31)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* SFTP支持输入路径跳转
|
|
||||||
|
|
||||||
## [2.2.3](https://github.com/chaos-zhu/easynode/releases) (2024-08-20)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 添加环境变量 ✔
|
|
||||||
* 支持IP访问白名单设置 ✔
|
|
||||||
* 修复一些小bug ✔
|
|
||||||
* 优化Eslint规则 ✔
|
|
||||||
|
|
||||||
## [2.2.2](https://github.com/chaos-zhu/easynode/releases) (2024-08-19)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 支持菜单栏的折叠与展开 ✔
|
|
||||||
* 优化终端回显 ✔
|
|
||||||
* 优化暗黑模式下滚动条样式 ✔
|
|
||||||
|
|
||||||
## [2.2.1](https://github.com/chaos-zhu/easynode/releases) (2024-08-18)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 支持暗黑主题切换 ✔
|
|
||||||
* 批量脚本下发执行结果通知重复的bug ✔
|
|
||||||
* 修复交换内存占比的bug ✔
|
|
||||||
* 优化服务端代码引用 ✔
|
|
||||||
* 修复Code scanning提到的依赖风险 ✔
|
|
||||||
|
|
||||||
## [2.2.0](https://github.com/chaos-zhu/easynode/releases) (2024-08-17)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 重构通知模块 ✔
|
|
||||||
* 支持大多数邮箱SMTP配置通知 ✔
|
|
||||||
* 支持Server酱通知 ✔
|
|
||||||
* 新增批量指令执行结果提醒 ✔
|
|
||||||
* 新增终端登录与登录状态提醒 ✔
|
|
||||||
* 新增服务器到期提醒 ✔
|
|
||||||
* 修复上传同一个文件无法选择的bug ✔
|
|
||||||
* 修复终端连接失败抛出异常的bug ✔
|
|
||||||
* 调整客户端安装脚本 ✔
|
|
||||||
|
|
||||||
## [2.1.9](https://github.com/chaos-zhu/easynode/releases) (2024-08-16)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 过滤客户端检测更新 ✔
|
|
||||||
|
|
||||||
## [2.1.8](https://github.com/chaos-zhu/easynode/releases) (2024-08-15)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 终端连接逻辑重写,断线自动重连 ✔
|
|
||||||
* 终端连接状态展示 ✔
|
|
||||||
* 终端支持选中复制&右键粘贴 ✔
|
|
||||||
* 终端设置支持字体大小 ✔
|
|
||||||
* 终端默认字体样式更改为`Cascadia Code` ✔
|
|
||||||
|
|
||||||
## [2.1.7](https://github.com/chaos-zhu/easynode/releases) (2024-08-14)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 客户端监控服务支持swap内存交换回传 ✔
|
|
||||||
* 面板支持展示swap内存交换状态展示 ✔
|
|
||||||
* 添加初始账户登录警告 ✔
|
|
||||||
|
|
||||||
## [2.1.6](https://github.com/chaos-zhu/easynode/releases) (2024-08-13)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* SFTP支持上传嵌套文件夹 ✔
|
|
||||||
* 修复面板服务缓存文件夹偶尔不存在的bug ✔
|
|
||||||
|
|
||||||
## [2.1.5](https://github.com/chaos-zhu/easynode/releases) (2024-08-12)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 新增终端设置 ✔
|
|
||||||
* 支持更多终端主题 ✔
|
|
||||||
* 支持终端背景图片(当前版本只缓存在前端且只可以使用内置背景图片) ✔
|
|
||||||
|
|
||||||
## [2.1.4](https://github.com/chaos-zhu/easynode/releases) (2024-08-12)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 新增cd全路径命令联动SFTP面板 ✔
|
|
||||||
* 修复SFTP文件编辑文件名称显示错误的bug ✔
|
|
||||||
|
|
||||||
## [2.1.3](https://github.com/chaos-zhu/easynode/releases) (2024-08-11)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 修复开启or关闭SFTP功能开关时,终端光标位置错误的bug ✔
|
|
||||||
|
|
||||||
## [2.1.2](https://github.com/chaos-zhu/easynode/releases) (2024-08-09)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 新增导入导出功能(EasyNode JSON) ✔
|
|
||||||
* 新增服务器列表排序与排序缓存 ✔
|
|
||||||
* 优化客户端连接状态展示 ✔
|
|
||||||
* 优化版本更新提示 ✔
|
|
||||||
|
|
||||||
## [2.1.1](https://github.com/chaos-zhu/easynode/releases) (2024-08-05)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 支持批量操作:批量修改实例通用信息(ssh配置等)、批量删除、批量安装客户端监控应用 ✔
|
|
||||||
* 自动化构建镜像 ✔
|
|
||||||
* 调整&优化面板UI ✔
|
|
||||||
* 内置常用脚本(逐渐添加中...) ✔
|
|
||||||
|
|
||||||
## [2.1.0](https://github.com/chaos-zhu/easynode/releases) (2024-08-02)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 支持脚本库功能 ✔
|
|
||||||
* 支持批量指令下发功能 ✔
|
|
||||||
* 支持多会话同步指令 ✔
|
|
||||||
* 重写Dockerfile,大幅减少镜像体积 ✔
|
|
||||||
* 调整优化面板UI ✔
|
|
||||||
|
|
||||||
## [2.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-07-29)
|
## [2.0.0](https://github.com/chaos-zhu/easynode/releases) (2024-07-29)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
29
Dockerfile
@ -1,21 +1,14 @@
|
|||||||
FROM node:20.16-alpine3.20 AS builder_web
|
|
||||||
WORKDIR /easynode/web
|
|
||||||
COPY ./web .
|
|
||||||
COPY yarn.lock .
|
|
||||||
RUN yarn
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM node:20.16-alpine3.20 AS builder_server
|
|
||||||
WORKDIR /easynode/server
|
|
||||||
COPY ./server .
|
|
||||||
COPY yarn.lock .
|
|
||||||
COPY --from=builder_web /easynode/web/dist ./app/static
|
|
||||||
RUN yarn
|
|
||||||
|
|
||||||
FROM node:20.16-alpine3.20
|
FROM node:20.16-alpine3.20
|
||||||
RUN apk add --no-cache iputils
|
|
||||||
WORKDIR /easynode
|
WORKDIR /easynode
|
||||||
COPY --from=builder_server /easynode/server .
|
COPY . .
|
||||||
ENV HOST=0.0.0.0
|
RUN yarn
|
||||||
|
|
||||||
|
WORKDIR /easynode/web
|
||||||
|
RUN yarn build
|
||||||
|
RUN find ../server/app/static -type f ! -name '.gitkeep' -exec rm -f {} +
|
||||||
|
RUN mv dist/* ../server/app/static
|
||||||
|
|
||||||
|
WORKDIR /easynode/server
|
||||||
|
ENV HOST 0.0.0.0
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
CMD ["npm", "start"]
|
CMD [ "npm", "start" ]
|
||||||
|
164
README.md
@ -1,123 +1,129 @@
|
|||||||
<div align="center">
|
|
||||||
|
|
||||||
# EasyNode
|
# EasyNode
|
||||||
|
|
||||||
_✨ 一个多功能Linux服务器WEB终端面板(webSSH&webSFTP) ✨_
|
> [!WARNING]
|
||||||
|
> 初次部署EasyNode,登录系统后务必记得修改默认账户密码 `admin/admin`!
|
||||||
|
|
||||||
</div>
|
> [!WARNING]
|
||||||
|
> 强烈建议使用 **iptables** 或 **fail2ban** 等安全服务限制IP访问,谨慎暴露面板服务到公网。
|
||||||
|
|
||||||
<p align="center">
|
> [!NOTE]
|
||||||
<a href="https://github.com/chaos-zhu/easynode/releases/latest">
|
> 客户端信息监控与webssh功能都将以`该服务器作为中转`。中国大陆连接建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端
|
||||||
<img src="https://img.shields.io/github/v/release/chaos-zhu/easynode?color=brightgreen" alt="release">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/chaos-zhu/easynode/actions">
|
|
||||||
<img src="https://img.shields.io/github/actions/workflow/status/chaos-zhu/easynode/docker-builder.yml?branch=main" alt="deployment status">
|
|
||||||
</a>
|
|
||||||
<a href="https://hub.docker.com/repository/docker/chaoszhu/easynode">
|
|
||||||
<img src="https://img.shields.io/docker/pulls/chaoszhu/easynode?color=brightgreen" alt="docker pull">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/chaos-zhu/easynode/releases/latest">
|
|
||||||
<img src="https://img.shields.io/github/downloads/chaos-zhu/easynode/total?color=brightgreen&include_prereleases" alt="release">
|
|
||||||
</a>
|
|
||||||
<a href="https://raw.githubusercontent.com/chaos-zhu/easynode/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/chaos-zhu/easynode?color=brightgreen" alt="license">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
- [功能](#功能)
|
||||||
<a href="#功能">功能</a>
|
- [安装](#安装指南)
|
||||||
·
|
- [服务端安装](#服务端安装)
|
||||||
<a href="#面板展示">面板展示</a>
|
- [客户端安装](#客户端安装)
|
||||||
·
|
- [版本日志](#版本日志)
|
||||||
<a href="#项目部署">项目部署</a>
|
- [安全与说明](#安全与说明)
|
||||||
·
|
- [开发](#开发)
|
||||||
<a href="#监控服务安装">监控服务安装</a>
|
- [Q&A](#qa)
|
||||||
·
|
- [感谢Star](#感谢star)
|
||||||
<a href="#安全与建议">安全与建议</a>
|
- [License](#license)
|
||||||
·
|
|
||||||
<a href="#常见问题">常见问题</a>
|
|
||||||
<!-- ·
|
|
||||||
<a href="#Plus功能">Plus版功能</a> -->
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
+ [x] 功能完善的**SSH终端**&**SFTP**
|
- [x] webssh终端&SFTP
|
||||||
+ [x] 批量导入、导出、编辑服务器配置、脚本等
|
- [x] 批量导入(Xshell&FinalShell)
|
||||||
+ [x] 脚本库
|
- [x] 实例分组
|
||||||
+ [x] 实例分组
|
- [x] 凭据托管
|
||||||
+ [x] 凭据托管
|
- [x] 邮件通知
|
||||||
+ [x] 多渠道通知
|
- [x] 实例状态推送
|
||||||
+ [x] 批量下发指令
|
- [ ] 批量指令(开发中)
|
||||||
+ [x] 自定义终端主题
|
- [ ] 脚本库(开发中)
|
||||||
|
|
||||||
## 面板展示
|
- 实例面板
|
||||||
|

|
||||||
|
- 终端&sftp
|
||||||
|

|
||||||
|
|
||||||

|
## 安装
|
||||||
|
|
||||||
## 项目部署
|
### 服务端安装
|
||||||
|
|
||||||
- 默认账户密码 `admin/admin`
|
- 占用端口:8082 推荐使用docker镜像安装
|
||||||
- web端口:8082
|
|
||||||
|
|
||||||
### docker镜像
|
#### Docker
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode
|
docker run -d --net=host --name=easynode-server -v $PWD/easynode/db:/easynode/server/app/db chaoszhu/easynode
|
||||||
```
|
```
|
||||||
环境变量:
|
访问:http://yourip:8082
|
||||||
- `DEBUG`: 启动debug日志 0:关闭 1:开启, 默认关闭
|
|
||||||
- `ALLOWED_IPS`: 可以访问服务的IP白名单, 多个使用逗号分隔, 支持填写部分ip前缀, 例如: `-e ALLOWED_IPS=127.0.0.1,196.168`
|
|
||||||
|
|
||||||
## 监控服务安装
|
#### 手动部署
|
||||||
|
|
||||||
- 监控服务用于实时向服务端&web端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息
|
依赖Nodejs版本 > 20+
|
||||||
|
|
||||||
- 默认端口:**22022**
|
```shell
|
||||||
|
git clone https://github.com/chaos-zhu/easynode
|
||||||
|
cd easynode
|
||||||
|
yarn
|
||||||
|
cd web
|
||||||
|
yarn build
|
||||||
|
mv dist/* ../server/app/static
|
||||||
|
cd ../server
|
||||||
|
yarn start
|
||||||
|
# 后台运行需安装pm2
|
||||||
|
pm2 start index.js --name easynode-server
|
||||||
|
```
|
||||||
|
|
||||||
|
访问:http://yourip:8082
|
||||||
|
|
||||||
|
- 查看日志:`pm2 log easynode-server`
|
||||||
|
- 启动服务:`pm2 start easynode-server`
|
||||||
|
- 停止服务:`pm2 stop easynode-server`
|
||||||
|
- 删除服务:`pm2 delete easynode-server`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 客户端安装
|
||||||
|
|
||||||
|
- 客户端用于实时向服务端推送**系统、公网IP、CPU、内存、硬盘、网卡**等基础信息,不安装不影响使用面板,但是无法实时同步基础信息。
|
||||||
|
|
||||||
|
- 占用端口:**22022**
|
||||||
|
|
||||||
> 安装
|
> 安装
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 使用默认端口22022安装
|
wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash
|
|
||||||
|
|
||||||
# 使用自定义端口安装, 例如54321
|
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 卸载
|
> 卸载
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
> 查看监控服务状态:`systemctl status easynode-client`
|
> 查看客户端状态:`systemctl status easynode-client`
|
||||||
>
|
>
|
||||||
> 查看监控服务日志: `journalctl --follow -u easynode-client`
|
> 查看客户端日志: `journalctl --follow -u easynode-client`
|
||||||
>
|
>
|
||||||
> 查看详细日志:journalctl -xe
|
> 查看详细日志:journalctl -xe
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 版本日志
|
||||||
|
|
||||||
## 安全与建议
|
- [CHANGELOG](./CHANGELOG.md)
|
||||||
|
|
||||||
首先声明,任何系统无法保障没有bug的存在,EasyNode也一样。
|
## 安全与说明
|
||||||
|
|
||||||
面板提供MFA2功能,并且可配置访问此服务的IP白名单, 如需加强可以使用**iptables**进一步限制IP访问。
|
> 本人非专业后端,此服务全凭兴趣开发. 由于知识受限,并不能保证没有漏洞的存在,所以请务必使用`iptables`限制ip访问该服务,且不要轻易暴露此服务在公网。
|
||||||
如果需要更高级别的安全性,建议面板服务不要暴露到公网。
|
|
||||||
|
|
||||||
webssh与监控服务都将以`该服务器作为中转`。中国大陆用户建议使用香港、新加坡、日本、韩国等地区的低延迟服务器来安装服务端面板
|
## 开发
|
||||||
|
|
||||||
## 常见问题
|
1. 拉取代码,环境 `nodejs``>=20`
|
||||||
|
2. cd到项目根目录,`yarn install` 执行安装依赖
|
||||||
|
3. `yarn dev`启动项目
|
||||||
|
4. web: `http://localhost:18090/`
|
||||||
|
|
||||||
- [QA](./Q%26A.md)
|
## Q&A
|
||||||
|
|
||||||
<!-- ## Plus版功能
|
- [Q&A](./Q%26A.md)
|
||||||
|
|
||||||
- 跳板机功能,拯救被墙实例与龟速终端输入
|
## 感谢Star
|
||||||
- 本地socket断开自动重连,无需手动重新连接
|
|
||||||
- 批量修改实例配置(优化版)
|
- 你的Star是我更新的动力,感谢~
|
||||||
- 脚本库批量导出导入
|
|
||||||
- 凭据管理支持解密带密码保护的密钥
|
## License
|
||||||
- 提出的功能需求享有更高的开发优先级 -->
|
|
||||||
|
[MIT](LICENSE). Copyright (c).
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
defaultPort: 22022
|
httpPort: 22022
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
let exec = require('child_process').exec
|
|
||||||
let os = require('os')
|
|
||||||
|
|
||||||
function getSwapMemory() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
// Windows-specific command
|
|
||||||
const command = 'powershell -command "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVirtualMemorySize, FreeVirtualMemory"'
|
|
||||||
exec(command, { encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('exec error:', error)
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
console.error('stderr:', stderr)
|
|
||||||
return reject(stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stdout.trim().split('\n')
|
|
||||||
const values = lines[lines.length - 1].trim().split(/\s+/)
|
|
||||||
const totalVirtualMemory = parseInt(values[0], 10) / 1024
|
|
||||||
const freeVirtualMemory = parseInt(values[1], 10) / 1024
|
|
||||||
const usedVirtualMemory = totalVirtualMemory - freeVirtualMemory
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
swapTotal: totalVirtualMemory,
|
|
||||||
swapFree: freeVirtualMemory,
|
|
||||||
swapUsed: usedVirtualMemory,
|
|
||||||
swapPercentage: ((usedVirtualMemory / totalVirtualMemory) * 100).toFixed(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
exec('free -m | grep Swap', (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('exec error:', error)
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
console.error('stderr:', stderr)
|
|
||||||
return reject(stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
const swapInfo = stdout.trim().split(/\s+/)
|
|
||||||
const swapTotal = parseInt(swapInfo[1], 10)
|
|
||||||
const swapUsed = parseInt(swapInfo[2], 10)
|
|
||||||
const swapFree = parseInt(swapInfo[3], 10)
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
swapTotal,
|
|
||||||
swapUsed,
|
|
||||||
swapFree,
|
|
||||||
swapPercentage: ((swapUsed / swapTotal) * 100).toFixed(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = getSwapMemory
|
|
@ -1,15 +1,14 @@
|
|||||||
const http = require('http')
|
const http = require('http')
|
||||||
const Koa = require('koa')
|
const Koa = require('koa')
|
||||||
const { defaultPort } = require('./config')
|
const { httpPort } = require('./config')
|
||||||
const wsOsInfo = require('./socket/monitor')
|
const wsOsInfo = require('./socket/monitor')
|
||||||
|
|
||||||
const httpServer = () => {
|
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)
|
||||||
const port = process.env.clientPort || defaultPort
|
server.listen(httpPort, () => {
|
||||||
server.listen(port, () => {
|
console.log(`Server(http) is running on port:${ httpPort }`)
|
||||||
console.log(`Server(http) is running on port:${ port }`)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const osu = require('node-os-utils')
|
const osu = require('node-os-utils')
|
||||||
const osSwap = require('../lib/swap')
|
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
|
|
||||||
let cpu = osu.cpu
|
let cpu = osu.cpu
|
||||||
@ -10,7 +9,7 @@ let osuOs = osu.os
|
|||||||
let users = osu.users
|
let users = osu.users
|
||||||
|
|
||||||
async function cpuInfo() {
|
async function cpuInfo() {
|
||||||
let cpuUsage = await cpu.usage(500)
|
let cpuUsage = await cpu.usage(200)
|
||||||
let cpuCount = cpu.count()
|
let cpuCount = cpu.count()
|
||||||
let cpuModel = cpu.model()
|
let cpuModel = cpu.model()
|
||||||
return {
|
return {
|
||||||
@ -27,13 +26,6 @@ async function memInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function swapInfo() {
|
|
||||||
let swapInfo = await osSwap()
|
|
||||||
return {
|
|
||||||
...swapInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function driveInfo() {
|
async function driveInfo() {
|
||||||
let driveInfo = {}
|
let driveInfo = {}
|
||||||
try {
|
try {
|
||||||
@ -79,7 +71,6 @@ module.exports = async () => {
|
|||||||
data = {
|
data = {
|
||||||
cpuInfo: await cpuInfo(),
|
cpuInfo: await cpuInfo(),
|
||||||
memInfo: await memInfo(),
|
memInfo: await memInfo(),
|
||||||
swapInfo: await swapInfo(),
|
|
||||||
driveInfo: await driveInfo(),
|
driveInfo: await driveInfo(),
|
||||||
netstatInfo: await netstatInfo(),
|
netstatInfo: await netstatInfo(),
|
||||||
osInfo: await osInfo(),
|
osInfo: await osInfo(),
|
||||||
|
@ -5,18 +5,12 @@ if [ "$(id -u)" != "0" ] ; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
clientPort=${clientPort:-22022}
|
|
||||||
SERVER_NAME=easynode-client
|
SERVER_NAME=easynode-client
|
||||||
FILE_PATH=/root/local/easynode-client
|
FILE_PATH=/root/local/easynode-client
|
||||||
SERVICE_PATH=/etc/systemd/system
|
SERVICE_PATH=/etc/systemd/system
|
||||||
CLIENT_VERSION=client-2024-10-13 # 目前监控客户端版本发布需手动更改为最新版本号
|
SERVER_VERSION=v1.0
|
||||||
SERVER_PROXY="https://git.221022.xyz/"
|
|
||||||
|
|
||||||
if [ ! -z "$1" ]; then
|
echo "***********************开始安装 easynode-client_${SERVER_VERSION}***********************"
|
||||||
clientPort=$1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "***********************开始安装EasyNode监控客户端端,当前版本号: ${CLIENT_VERSION}, 端口: ${clientPort}***********************"
|
|
||||||
|
|
||||||
systemctl status ${SERVER_NAME} > /dev/null 2>&1
|
systemctl status ${SERVER_NAME} > /dev/null 2>&1
|
||||||
if [ $? != 4 ]
|
if [ $? != 4 ]
|
||||||
@ -48,16 +42,16 @@ echo "***********************创建文件PATH***********************"
|
|||||||
mkdir -p ${FILE_PATH}
|
mkdir -p ${FILE_PATH}
|
||||||
|
|
||||||
echo "***********************下载开始***********************"
|
echo "***********************下载开始***********************"
|
||||||
|
DOWNLOAD_SERVICE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client.service"
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
echo "***********************系统架构: $ARCH***********************"
|
|
||||||
if [ "$ARCH" = "x86_64" ] ; then
|
if [ "$ARCH" = "x86_64" ] ; then
|
||||||
DOWNLOAD_FILE_URL="${SERVER_PROXY}https://github.com/chaos-zhu/easynode/releases/download/${CLIENT_VERSION}/easynode-client-x64"
|
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-x86"
|
||||||
elif [ "$ARCH" = "aarch64" ] ; then
|
elif [ "$ARCH" = "aarch64" ] ; then
|
||||||
DOWNLOAD_FILE_URL="${SERVER_PROXY}https://github.com/chaos-zhu/easynode/releases/download/${CLIENT_VERSION}/easynode-client-arm64"
|
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-arm64"
|
||||||
else
|
else
|
||||||
echo "不支持的架构:$ARCH. 只支持x86_64和aarch64,其他架构请自行构建"
|
echo "未知的架构:$ARCH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -69,8 +63,6 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DOWNLOAD_SERVICE_URL="${SERVER_PROXY}https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client.service"
|
|
||||||
|
|
||||||
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
|
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
|
||||||
|
|
||||||
if [ $? != 0 ]
|
if [ $? != 0 ]
|
||||||
@ -85,8 +77,6 @@ echo "***********************下载成功***********************"
|
|||||||
chmod +x ${FILE_PATH}/${SERVER_NAME}
|
chmod +x ${FILE_PATH}/${SERVER_NAME}
|
||||||
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
|
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
|
||||||
|
|
||||||
sed -i "s/clientPort=22022/clientPort=${clientPort}/g" ${FILE_PATH}/${SERVER_NAME}.service
|
|
||||||
|
|
||||||
# echo "***********************移动service&reload***********************"
|
# echo "***********************移动service&reload***********************"
|
||||||
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
|
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
|
||||||
|
|
||||||
@ -96,6 +86,7 @@ systemctl daemon-reload
|
|||||||
echo "***********************启动服务***********************"
|
echo "***********************启动服务***********************"
|
||||||
systemctl start ${SERVER_NAME}
|
systemctl start ${SERVER_NAME}
|
||||||
|
|
||||||
|
|
||||||
# echo "***********************设置开机启动***********************"
|
# echo "***********************设置开机启动***********************"
|
||||||
systemctl enable ${SERVER_NAME}
|
systemctl enable ${SERVER_NAME}
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=easynode client server
|
Description=easynode client server port_22022
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Environment="clientPort=22022"
|
|
||||||
ExecStart=/root/local/easynode-client/easynode-client
|
ExecStart=/root/local/easynode-client/easynode-client
|
||||||
WorkingDirectory=/root/local/easynode-client
|
WorkingDirectory=/root/local/easynode-client
|
||||||
Restart=always
|
Restart=always
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "easynode-client",
|
"name": "easynode-client",
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"description": "easynode-client",
|
"description": "easynode-client",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@ -8,9 +8,9 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"client": "nodemon ./app/main.js",
|
"client": "nodemon ./app/main.js",
|
||||||
"pkgwin": "pkg . -t node16-win-x64",
|
"pkg": "pkg .",
|
||||||
"pkglinux:x64": "pkg . -t node16-linux-x64 -o dist/easynode-client-x64",
|
"pkglinux:x86": "pkg . -t node18-linux-x64",
|
||||||
"pkglinux:arm64": "pkg . -t node16-linux-arm64 -o dist/easynode-client-arm64"
|
"pkglinux:arm": "pkg . -t node18"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
BIN
doc_images/1.jpg
Before Width: | Height: | Size: 132 KiB |
BIN
doc_images/2.jpg
Before Width: | Height: | Size: 138 KiB |
BIN
doc_images/3.jpg
Before Width: | Height: | Size: 120 KiB |
BIN
doc_images/4.jpg
Before Width: | Height: | Size: 52 KiB |
BIN
doc_images/5.jpg
Before Width: | Height: | Size: 75 KiB |
BIN
doc_images/6.jpg
Before Width: | Height: | Size: 117 KiB |
BIN
doc_images/7.jpg
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 900 KiB |
BIN
doc_images/v2.0-1.jpg
Normal file
After Width: | Height: | Size: 240 KiB |
BIN
doc_images/v2.0-2.jpg
Normal file
After Width: | Height: | Size: 247 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "easynode",
|
"name": "easynode",
|
||||||
|
"version": "2.0.0",
|
||||||
"description": "web ssh",
|
"description": "web ssh",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@ -22,8 +23,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"yarn workspace web run dev\" \"yarn workspace server run local\"",
|
"dev": "concurrently \"yarn workspace web run dev\" \"yarn workspace server run local\"",
|
||||||
"clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules",
|
"clean": "rimraf web/node_modules server/node_modules client/node_modules node_modules"
|
||||||
"encrypt": "node ./local-script/encrypt-file.js"
|
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/chaos-zhu/easynode/issues"
|
"url": "https://github.com/chaos-zhu/easynode/issues"
|
||||||
|
@ -1,8 +1,2 @@
|
|||||||
# 启动debug日志 0:关闭 1:开启
|
# 启动debug日志 0:关闭 1:开启
|
||||||
DEBUG=1
|
DEBUG=1
|
||||||
|
|
||||||
# 访问IP限制
|
|
||||||
allowedIPs=['127.0.0.1']
|
|
||||||
|
|
||||||
# 激活PLUS功能的授权码
|
|
||||||
PLUS_KEY=
|
|
||||||
|
@ -28,15 +28,10 @@ module.exports = {
|
|||||||
quotes: ['error', 'single'], // 引号:single单引 double双引
|
quotes: ['error', 'single'], // 引号:single单引 double双引
|
||||||
semi: ['error', 'never'], // 结尾分号:never禁止 always必须
|
semi: ['error', 'never'], // 结尾分号:never禁止 always必须
|
||||||
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
|
'comma-dangle': ['error', 'never'], // 对象拖尾逗号
|
||||||
'space-before-blocks': ['error', 'always'],
|
|
||||||
'space-in-parens': ['error', 'never'],
|
|
||||||
'keyword-spacing': ['error', { 'before': true, 'after': true }],
|
|
||||||
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
|
'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明
|
||||||
'no-multi-assign': 0,
|
'no-multi-assign': 0,
|
||||||
'no-restricted-globals': 0,
|
'no-restricted-globals': 0,
|
||||||
'no-case-declarations': 0,
|
|
||||||
'space-before-function-paren': 0, // 函数定义时括号前面空格
|
'space-before-function-paren': 0, // 函数定义时括号前面空格
|
||||||
'no-async-promise-executor': 0, // 允许在回调中使用async函数
|
|
||||||
'one-var': 0, // 允许连续声明
|
'one-var': 0, // 允许连续声明
|
||||||
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
|
// 'no-undef': 0, // 允许未定义的变量【会使env配置无效】
|
||||||
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
|
'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】
|
||||||
|
@ -4,20 +4,17 @@ consola.info('debug日志:', process.env.DEBUG === '1' ? '开启' : '关闭')
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
httpPort: 8082,
|
httpPort: 8082,
|
||||||
defaultClientPort: 22022,
|
clientPort: 22022, // 暂不支持更改
|
||||||
uploadDir: path.join(process.cwd(),'app/db'),
|
uploadDir: path.join(process.cwd(),'app/db'),
|
||||||
staticDir: path.join(process.cwd(),'app/static'),
|
staticDir: path.join(process.cwd(),'app/static'),
|
||||||
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
|
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
|
||||||
credentialsDBPath: path.join(process.cwd(),'app/db/credentials.db'),
|
credentialsDBPath: path.join(process.cwd(),'app/db/credentials.db'),
|
||||||
keyDBPath: path.join(process.cwd(),'app/db/key.db'),
|
keyDBPath: path.join(process.cwd(),'app/db/key.db'),
|
||||||
hostListDBPath: path.join(process.cwd(),'app/db/host.db'),
|
hostListDBPath: path.join(process.cwd(),'app/db/host.db'),
|
||||||
|
notifyConfDBPath: path.join(process.cwd(),'app/db/notify.db'),
|
||||||
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
||||||
|
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
|
||||||
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
|
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
|
||||||
notifyDBPath: path.join(process.cwd(),'app/db/notify.db'),
|
|
||||||
notifyConfigDBPath: path.join(process.cwd(),'app/db/notify-config.db'),
|
|
||||||
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.db'),
|
|
||||||
logDBPath: path.join(process.cwd(),'app/db/log.db'),
|
|
||||||
plusDBPath: path.join(process.cwd(),'app/db/plus.db'),
|
|
||||||
apiPrefix: '/api/v1',
|
apiPrefix: '/api/v1',
|
||||||
logConfig: {
|
logConfig: {
|
||||||
outDir: path.join(process.cwd(),'./app/logs'),
|
outDir: path.join(process.cwd(),'./app/logs'),
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "easynode监控服务安装",
|
|
||||||
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash",
|
|
||||||
"description": "easynode-监控服务-安装脚本"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "easynode监控服务卸载",
|
|
||||||
"command": "curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash",
|
|
||||||
"description": "easynode-监控服务-卸载脚本"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "查询本机公网IP",
|
|
||||||
"command": "curl ifconfig.me",
|
|
||||||
"description": "查询本机公网IP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "生成ssh密钥对",
|
|
||||||
"command": "ssh-keygen -t rsa -b 2048",
|
|
||||||
"description": "生成ssh密钥对"
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,11 +1,10 @@
|
|||||||
const { HostListDB, GroupDB } = require('../utils/db-class')
|
const { readGroupList, writeGroupList, readHostList, writeHostList, randomStr } = require('../utils')
|
||||||
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
const groupDB = new GroupDB().getInstance()
|
|
||||||
|
|
||||||
async function getGroupList({ res }) {
|
async function getGroupList({ res }) {
|
||||||
let data = await groupDB.findAsync({})
|
let data = await readGroupList()
|
||||||
data = data.map(item => ({ ...item, id: item._id }))
|
data = data.map(item => {
|
||||||
|
return { ...item, id: item._id }
|
||||||
|
})
|
||||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
@ -13,8 +12,10 @@ async function getGroupList({ res }) {
|
|||||||
const addGroupList = async ({ res, request }) => {
|
const addGroupList = async ({ res, request }) => {
|
||||||
let { body: { name, index } } = request
|
let { body: { name, index } } = request
|
||||||
if (!name) return res.fail({ data: false, msg: '参数错误' })
|
if (!name) return res.fail({ data: false, msg: '参数错误' })
|
||||||
|
let groupList = await readGroupList()
|
||||||
let group = { name, index }
|
let group = { name, index }
|
||||||
await groupDB.insertAsync(group)
|
groupList.push(group)
|
||||||
|
await writeGroupList(groupList)
|
||||||
res.success({ data: '添加成功' })
|
res.success({ data: '添加成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,26 +23,34 @@ const updateGroupList = async ({ res, request }) => {
|
|||||||
let { params: { id } } = request
|
let { params: { id } } = request
|
||||||
let { body: { name, index } } = request
|
let { body: { name, index } } = request
|
||||||
if (!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
if (!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
||||||
let target = await groupDB.findOneAsync({ _id: id })
|
let groupList = await readGroupList()
|
||||||
if (!target) return res.fail({ data: false, msg: `分组ID${ id }不存在` })
|
let idx = groupList.findIndex(item => item._id === id)
|
||||||
await groupDB.updateAsync({ _id: id }, { name, index: Number(index) || 0 })
|
if (idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` })
|
||||||
|
const { _id } = groupList[idx]
|
||||||
|
let group = { _id, name, index: Number(index) || 0 }
|
||||||
|
groupList.splice(idx, 1, group)
|
||||||
|
await writeGroupList(groupList)
|
||||||
res.success({ data: '修改成功' })
|
res.success({ data: '修改成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeGroup = async ({ res, request }) => {
|
const removeGroup = async ({ res, request }) => {
|
||||||
let { params: { id } } = request
|
let { params: { id } } = request
|
||||||
if (id === 'default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
if (id === 'default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
||||||
|
let groupList = await readGroupList()
|
||||||
|
let idx = groupList.findIndex(item => item._id === id)
|
||||||
|
if (idx === -1) return res.fail({ msg: '分组不存在' })
|
||||||
|
|
||||||
// 移除分组将所有该分组下host分配到default中去
|
// 移除分组将所有该分组下host分配到default中去
|
||||||
let hostList = await hostListDB.findAsync({})
|
let hostList = await readHostList()
|
||||||
if (Array.isArray(hostList) && hostList.length > 0) {
|
hostList = hostList?.map((item) => {
|
||||||
for (let item of hostList) {
|
if (item.group === groupList[idx]._id) item.group = 'default'
|
||||||
if (item.group === id) {
|
return item
|
||||||
item.group = 'default'
|
})
|
||||||
await hostListDB.updateAsync({ _id: item._id }, item)
|
await writeHostList(hostList)
|
||||||
}
|
|
||||||
}
|
groupList.splice(idx, 1)
|
||||||
}
|
await writeGroupList(groupList)
|
||||||
await groupDB.removeAsync({ _id: id })
|
|
||||||
res.success({ data: '移除成功' })
|
res.success({ data: '移除成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,97 +1,106 @@
|
|||||||
const path = require('path')
|
const { readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = require('../utils')
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
|
||||||
const { HostListDB } = require('../utils/db-class')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
|
|
||||||
async function getHostList({ res }) {
|
async function getHostList({ res }) {
|
||||||
let data = await hostListDB.findAsync({})
|
// console.log('get-host-list')
|
||||||
|
let data = await readHostList()
|
||||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
try {
|
let { username, port, authType, _id: id, credential } = item
|
||||||
let { authType, _id: id, credential } = item
|
// console.log('解密凭证title: ', credential)
|
||||||
if (credential) credential = await AESDecryptAsync(credential)
|
if (credential) credential = await AESDecryptSync(credential)
|
||||||
const isConfig = Boolean(authType && item[authType])
|
const isConfig = Boolean(username && port && (item[authType]))
|
||||||
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||||
} catch (error) {
|
|
||||||
consola.error('getHostList error: ', error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addHost({ res, request }) {
|
async function addHost({
|
||||||
let { body } = request
|
res, request
|
||||||
if (!body.name || !body.host) return res.fail({ msg: 'missing params: name or host' })
|
}) {
|
||||||
let newRecord = { ...body }
|
let {
|
||||||
const { authType, tempKey } = newRecord
|
body: {
|
||||||
if (newRecord[authType] && tempKey) {
|
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
port, username, authType, password, privateKey, credential, command, tempKey
|
||||||
const clearSSHKey = await AESDecryptAsync(newRecord[authType], clearTempKey)
|
|
||||||
newRecord[authType] = await AESEncryptAsync(clearSSHKey)
|
|
||||||
}
|
}
|
||||||
await hostListDB.insertAsync(newRecord)
|
} = request
|
||||||
|
// console.log(request)
|
||||||
|
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||||
|
let hostList = await readHostList()
|
||||||
|
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||||
|
let record = {
|
||||||
|
name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, credential, command
|
||||||
|
}
|
||||||
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
|
console.log('clearTempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
|
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
|
hostList.push(record)
|
||||||
|
await writeHostList(hostList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHost({ res, request }) {
|
async function updateHost({
|
||||||
|
res, request
|
||||||
|
}) {
|
||||||
let {
|
let {
|
||||||
body
|
body: {
|
||||||
|
host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
|
port, username, authType, password, privateKey, credential, command, tempKey
|
||||||
|
}
|
||||||
} = request
|
} = request
|
||||||
if (typeof body !== 'object') return res.fail({ msg: '参数错误' })
|
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||||
const updateFiled = { ...body }
|
let hostList = await readHostList()
|
||||||
const { id, authType, tempKey } = updateFiled
|
let record = {
|
||||||
if (authType && updateFiled[authType]) {
|
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
port, username, authType, password, privateKey, credential, command
|
||||||
const clearSSHKey = await AESDecryptAsync(updateFiled[authType], clearTempKey)
|
|
||||||
updateFiled[authType] = await AESEncryptAsync(clearSSHKey)
|
|
||||||
delete updateFiled.tempKey
|
|
||||||
} else {
|
|
||||||
delete updateFiled.authType
|
|
||||||
delete updateFiled.password
|
|
||||||
delete updateFiled.privateKey
|
|
||||||
delete updateFiled.credential
|
|
||||||
}
|
}
|
||||||
console.log('updateFiled: ', updateFiled)
|
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试添加实例` })
|
||||||
await hostListDB.updateAsync({ _id: id }, { $set: { ...updateFiled } })
|
|
||||||
res.success({ msg: '修改成功' })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function batchUpdateHost({ res, request }) {
|
let idx = hostList.findIndex(({ host }) => host === oldHost)
|
||||||
let { updateHosts } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
const oldRecord = hostList[idx]
|
||||||
if (updateHosts) {
|
// 如果存在原认证方式则保存下来
|
||||||
await updateHosts({ res, request })
|
if (!record[authType] && oldRecord[authType]) {
|
||||||
|
record[authType] = oldRecord[authType]
|
||||||
} else {
|
} else {
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能!' })
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
|
console.log('clearTempKey:', clearTempKey)
|
||||||
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
|
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
}
|
}
|
||||||
|
hostList.splice(idx, 1, record)
|
||||||
|
writeHostList(hostList)
|
||||||
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeHost({ res, request }) {
|
async function removeHost({
|
||||||
let { body: { ids } } = request
|
res, request
|
||||||
if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' })
|
}) {
|
||||||
const numRemoved = await hostListDB.removeAsync({ _id: { $in: ids } }, { multi: true })
|
let { body: { host } } = request
|
||||||
res.success({ data: `已移除,数量: ${ numRemoved }` })
|
let hostList = await readHostList()
|
||||||
|
let hostIdx = hostList.findIndex(item => item.host === host)
|
||||||
|
if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
||||||
|
hostList.splice(hostIdx, 1)
|
||||||
|
writeHostList(hostList)
|
||||||
|
res.success({ data: `${ host }已移除` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importHost({ res, request }) {
|
async function importHost({
|
||||||
let { body: { importHost, isEasyNodeJson = false } } = request
|
res, request
|
||||||
|
}) {
|
||||||
|
let { body: { importHost } } = request
|
||||||
if (!Array.isArray(importHost)) return res.fail({ msg: '参数错误' })
|
if (!Array.isArray(importHost)) return res.fail({ msg: '参数错误' })
|
||||||
let hostList = await hostListDB.findAsync({})
|
let hostList = await readHostList()
|
||||||
// 考虑到批量导入可能会重复太多,先过滤已存在的host:port
|
// 过滤已存在的host
|
||||||
let hostListSet = new Set(hostList.map(({ host, port }) => `${ host }:${ port }`))
|
let hostListSet = new Set(hostList.map(item => item.host))
|
||||||
let newHostList = importHost.filter(({ host, port }) => !hostListSet.has(`${ host }:${ port }`))
|
let newHostList = importHost.filter(item => !hostListSet.has(item.host))
|
||||||
let newHostListLen = newHostList.length
|
let newHostListLen = newHostList.length
|
||||||
if (newHostListLen === 0) return res.fail({ msg: '导入的实例已存在' })
|
if (newHostListLen === 0) return res.fail({ msg: '导入的实例已存在' })
|
||||||
|
|
||||||
if (isEasyNodeJson) {
|
|
||||||
newHostList = newHostList.map((item) => {
|
|
||||||
item.credential = ''
|
|
||||||
item.isConfig = false
|
|
||||||
delete item.id
|
|
||||||
delete item.isConfig
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let extraFiels = {
|
let extraFiels = {
|
||||||
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
expired: null, expiredNotify: false, group: 'default', consoleUrl: '', remark: '',
|
||||||
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
authType: 'privateKey', password: '', privateKey: '', credential: '', command: ''
|
||||||
@ -101,8 +110,8 @@ async function importHost({ res, request }) {
|
|||||||
item.index = newHostListLen - index
|
item.index = newHostListLen - index
|
||||||
return Object.assign(item, { ...extraFiels })
|
return Object.assign(item, { ...extraFiels })
|
||||||
})
|
})
|
||||||
}
|
hostList.push(...newHostList)
|
||||||
await hostListDB.insertAsync(newHostList)
|
writeHostList(hostList)
|
||||||
res.success({ data: { len: newHostList.length } })
|
res.success({ data: { len: newHostList.length } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +120,5 @@ module.exports = {
|
|||||||
addHost,
|
addHost,
|
||||||
updateHost,
|
updateHost,
|
||||||
removeHost,
|
removeHost,
|
||||||
importHost,
|
importHost
|
||||||
batchUpdateHost
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
const { LogDB } = require('../utils/db-class')
|
|
||||||
const logDB = new LogDB().getInstance()
|
|
||||||
|
|
||||||
let whiteList = process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(',') : []
|
|
||||||
|
|
||||||
async function getLog({ res }) {
|
|
||||||
let list = await logDB.findAsync({})
|
|
||||||
list = list.map(item => {
|
|
||||||
return { ...item, id: item._id }
|
|
||||||
})
|
|
||||||
list?.sort((a, b) => Number(b.date) - Number(a.date))
|
|
||||||
res.success({ data: { list, whiteList } })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getLog
|
|
||||||
}
|
|
@ -1,56 +1,89 @@
|
|||||||
const path = require('path')
|
const {
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
readSupportEmailList,
|
||||||
const { sendServerChan, sendEmail } = require('../utils/notify')
|
readUserEmailList,
|
||||||
const { NotifyConfigDB, NotifyDB } = require('../utils/db-class')
|
writeUserEmailList,
|
||||||
const notifyDB = new NotifyDB().getInstance()
|
emailTransporter,
|
||||||
const notifyConfigDB = new NotifyConfigDB().getInstance()
|
readNotifyList,
|
||||||
|
writeNotifyList } = require('../utils')
|
||||||
|
const commonTemp = require('../template/commonTemp')
|
||||||
|
|
||||||
async function getNotifyConfig({ res }) {
|
async function getSupportEmailList({ res }) {
|
||||||
const data = await notifyConfigDB.findOneAsync({})
|
const data = await readSupportEmailList()
|
||||||
return res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNotifyConfig({ res, request }) {
|
async function getUserEmailList({ res }) {
|
||||||
let { body: { noticeConfig } } = request
|
const userEmailList = (await readUserEmailList()).map(({ target, auth: { user } }) => ({ target, user }))
|
||||||
let { type } = noticeConfig
|
const supportEmailList = await readSupportEmailList()
|
||||||
try {
|
const data = userEmailList.map(({ target: userTarget, user: email }) => {
|
||||||
// console.log('noticeConfig: ', noticeConfig[type])
|
let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name
|
||||||
switch (type) {
|
return { name, email }
|
||||||
case 'sct':
|
})
|
||||||
await sendServerChan(noticeConfig[type]['sendKey'], 'EasyNode通知测试', '这是一条测试通知')
|
res.success({ data })
|
||||||
break
|
}
|
||||||
case 'email':
|
|
||||||
await sendEmail(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
|
async function pushEmail({ res, request }) {
|
||||||
break
|
let { body: { toEmail, isTest } } = request
|
||||||
case 'tg':
|
if (!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' })
|
||||||
let { sendTg } = await decryptAndExecuteAsync(path.join(__dirname, '../utils/plus.js')) || {}
|
consola.info('发送测试邮件:', toEmail)
|
||||||
console.log('sendTg: ', sendTg)
|
let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') })
|
||||||
if (!sendTg) return res.fail({ msg: 'Plus专属功能点,请激活Plus' })
|
msg = msg && msg.message || msg
|
||||||
await sendTg(noticeConfig[type], 'EasyNode通知测试', '这是一条测试通知')
|
if (code === 0) return res.success({ msg })
|
||||||
break
|
return res.fail({ msg })
|
||||||
}
|
}
|
||||||
await notifyConfigDB.update({}, { $set: noticeConfig }, { upsert: true })
|
|
||||||
return res.success({ msg: '测试通过 | 保存成功' })
|
async function updateUserEmailList({ res, request }) {
|
||||||
} catch (error) {
|
let { body: { target, auth } } = request
|
||||||
return res.fail({ msg: error.message })
|
const supportList = await 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 = await 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 } = await writeUserEmailList(userEmailList)
|
||||||
|
if (code === 0) return res.success()
|
||||||
|
return res.fail({ msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUserEmail({ res, request }) {
|
||||||
|
let { params: { email } } = request
|
||||||
|
const userEmailList = await readUserEmailList()
|
||||||
|
let idx = userEmailList.findIndex(({ auth: { user } }) => user === email)
|
||||||
|
if (idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` })
|
||||||
|
userEmailList.splice(idx, 1)
|
||||||
|
const { code, msg } = await writeUserEmailList(userEmailList)
|
||||||
|
if (code === 0) return res.success()
|
||||||
|
return res.fail({ msg })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNotifyList({ res }) {
|
async function getNotifyList({ res }) {
|
||||||
const data = await notifyDB.findAsync({})
|
const data = await readNotifyList()
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNotifyList({ res, request }) {
|
async function updateNotifyList({ res, request }) {
|
||||||
let { body: { type, sw } } = request
|
let { body: { type, sw } } = request
|
||||||
if (!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
if (!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
||||||
await notifyDB.updateAsync({ type }, { $set: { sw } })
|
const notifyList = await readNotifyList()
|
||||||
|
let target = notifyList.find((item) => item.type === type)
|
||||||
|
if (!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` })
|
||||||
|
target.sw = sw
|
||||||
|
// console.log(notifyList)
|
||||||
|
await writeNotifyList(notifyList)
|
||||||
res.success()
|
res.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getNotifyConfig,
|
pushEmail,
|
||||||
updateNotifyConfig,
|
getSupportEmailList,
|
||||||
|
getUserEmailList,
|
||||||
|
updateUserEmailList,
|
||||||
|
removeUserEmail,
|
||||||
getNotifyList,
|
getNotifyList,
|
||||||
updateNotifyList
|
updateNotifyList
|
||||||
}
|
}
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
const { OnekeyDB } = require('../utils/db-class')
|
|
||||||
const onekeyDB = new OnekeyDB().getInstance()
|
|
||||||
|
|
||||||
async function getOnekeyRecord({ res }) {
|
|
||||||
let data = await onekeyDB.findAsync({})
|
|
||||||
data = data.map(item => {
|
|
||||||
return { ...item, id: item._id }
|
|
||||||
})
|
|
||||||
data?.sort((a, b) => Number(b.date) - Number(a.date))
|
|
||||||
res.success({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeOnekeyRecord = async ({ res, request }) => {
|
|
||||||
let { body: { ids } } = request
|
|
||||||
if (ids === 'ALL') {
|
|
||||||
await onekeyDB.removeAsync({}, { multi: true })
|
|
||||||
res.success({ data: '移除全部成功' })
|
|
||||||
} else {
|
|
||||||
await onekeyDB.removeAsync({ _id: { $in: ids } })
|
|
||||||
res.success({ data: '移除成功' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getOnekeyRecord,
|
|
||||||
removeOnekeyRecord
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
U2FsdGVkX18Hh5ifqReKzxVcNwA8NC2cGnvuPCHW9V4+sVMxFFE7NxliY3R9Pyu2jZvnRb80+VpkEinfaZX0H1xx+I5PU2/mqIUU+1yxKrmWQtwJm6EwNwyDFrj3Epbl1zkfTUXLhk1a5lff+s1Qic02SbnLMtThV9Pg2m6w7HeJJiYOdaRFGlHvgGL4m7O9Ps135wdsdLU9y5aRiXF+1fi35Y6ZlDwPJGEMfZyIQKF87QksAW6LOP/Y1+mgIfLS6WwJnf8kW4l0KQktfvmsWtn00neZRQJc9I6WVMEN2jq4vbeE0KqtoOV0B/+Y/nLFnJjSYs5VE4qQ3gTFzuHe/dPoWXcBX5J5RhAxeY1qVQUtgKxUVwnBeGyjCmM7scX001AoxMcZFnpl+rx1ccOHYF2wB8GsuhsRlAAgWiyPXVJFSMYW3mFm61wvy1dWFad+kNYNFJo+SW8YUSkUCs3sXHXHn8eFsy75ChgHqMp1hvvyug8eFVPwp3IgtLK1D1Et096h8EhhvCvR7VecWwFi4AeMvuZWSmn+gkgGinx9zKUjkA5Bi65tyXmCa4ozyoi+TtuWKqJZyRQ8K2Kw0fc1AUCN8Cp/89Omb9thA10lvVtEJ+k1anao1llY9tPJsYlb0lNGYUlff29cDQnKIbV8P9mHXAyjRJatypWfLPfvqBT81iEDdB5dMASgm3gZqQPrSE50hBsCjzeNaCQF32TPfEFeOWRS1M1tOFpjanJZwfUreMLR77lANkSjiPYOgUvSzgAu0JVIehjXW2vYhC3+Sg7ETbdeV74pAx+Tc8qNWPyZtbNvdg+5wegr5ICgvXObf/btDUL9Jl7x+x7SY7dDrDj6AJRQROcUCdtNisG8HBKnvWS8nqNaUmR7d2E8pQ6qEFKX1ISvkxUp5RTD+9Vos0BfL4+mUB9iovxhDTfSXCIdJa69obTvvLD9xOJvNDrd72zLTQZSI74i/cFeNlersYiQAgL26oyqkv1eFL7Xd3bzq24EbZjP3hrBEqktW5qFeUAe8cPuA3bwDQwGI5BGkQ2hsS7G8xvx0dwllUOE3XVjxEuH8hkGO/GfFdqPRHfkizoNu1yNQEQeY6s9cMp5ovY20YIPRl8bhakjcUtUjMqee5kDdmScELKzoam8TwNiTBrBiuCwA2DcaC6dWDgOjhRs1Y4LEiQ8KZptuO/zTbJc5qcoKA6CUiVTN7vD4u5DHN60mGU9hoS46hfCe++U6L5FR4lafjRdUR0qkCEtf2SKnXyWqLUTgS2kNLQr4ZZbLMi7Mm+5+Q1JIIjzqqfjOlzeO9T4F9lknUkFXD5bc0Q5g+it89KG8xDbISUznv/UTXSxh485VKecT9Cjgd7I438N6xeL1CcJZiluLOvZ0Z3FDxkrW4Tmwbi852Z7tghFAamMW7GPL8LJRt5q2fhe0/U5oKBuGglRvga2tju3wBfzQdpavyNVyRjN2pywO4fk3qhez0suF9wVOc6GU9PUU2jCRm/gEF/qrj32tUjpDbxS9D1nCs441La7bYV8eCtb+2pEjgvjtIp+BM0lz+aHnseKT/iUGHlubKhrTMJQ7jEAPKtcl2OpS4fXiVIiy0qK4rI8S/vkdcRd07H77FfPDqEHTxTMQhHMGqi+d+YpFgrXSin4vcn8KXS87MEILjn5kmUDOsXAWZCqlD3oQ5ADVt91R+Ty5DcIZgaiQkB1aq6feIfSx4rsioNCOgFqmx4mcCds4Ar6gzsRdXN4Kcw8plrrePttZLyNOleoIX5Diy3GAiq6ENCkbYtsaic5EqCQ6AV5qBzEDu0DKZkdqxUWd0wf5+gJwEFQAMj+lD/UhlHuD8ArSI56jYQUbrcfdLnXutfrNA2Ogte9RltQxiUb6N90uNW1rT/2vlUgmQgbvZriKqpm+K3CZ9+6zsCDSUgr/cJmkSvu5gIpvC81IAQX6K8sUqtc9l3vn5vEvqqIp2yb5N25xs0NB0/yglyAHgXLXbG/sE73TrRMj4W+3HGlF35YSQnsLcyzvqEIoAhjngDf/6HXCkNpUQjyc8+uzIsKTh73WV9rh1/7xoY0lxHGabI+c8j5+WlWD1K0Xec83Sodqf+XStr90w1ceK73/DZGdgJIbdKfgO4Xn9ZY8AlzbeJq0W2/WWi/nPE9UZtVK6EEuOcmG2L5/gv2hTMjko6KG+ygrn0+bSvClXL51Brq7IvfO9mMlAGV8zK8vp82RM0KH38xPaJGTHbdawB1gaatkXywzXw0YTmzfaswt46WcWlLZ8vgr01zMp7pfp6A4GAT952rSprlfE014osCZj2oe+j2FQ0QOIYPSj3IatoqlDGfMOxPAbId8sx3anls9Zbk4feeVEvy0+VEmeZVIyDSzjZWuQYQ7VQLEcyaARRtOnfDYt2STIXy61ScWepdj1tmuhw/Kc0Aov61tEZ1apHHxrugzmN96A/2FST2KkbCtsYvbBqE9bZ3F4dLAfVazWidSQv4wPKgkZHFY94jlXxkN0dkA0yildyiQC5k3Iiw3zSwZO9a91K8uSQbbL54C4Y7aCW1HG//OabzNSg9Qty5a1hoiovpCiziAc3xoxuT+75ICozxKLG8+UN3vEZ2QXMv3b/qlXhRr7t8LtlFiA9nmUMfCAieovrZSB4OzrKHe37mg17USWsF1by73YTriFRTiE7JO5E6GMFz3bloppT64svf0SHgFELOuc4xclZfJTYAhLLxkiwDzmKWWheEz5TOOL/8p+5n7+/AuffGykVu6NlmSXH1uIg9JYNUy6UFnd2vOhx+8DxSVFd+1VdW+u2zpPAgiFAiNZJGx+6BVS05kO2mQ++0BHlmbXTw2tdt/BF1N07J5kIY0yRqrMtlwAb6cNbb+yWHkYX/C+3MDLBd
|
|
@ -1,73 +1,52 @@
|
|||||||
const path = require('path')
|
const { readScriptList, writeScriptList } = require('../utils')
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const { randomStr } = require('../utils/tools')
|
|
||||||
const { ScriptsDB } = require('../utils/db-class')
|
|
||||||
const localShellJson = require('../config/shell.json')
|
|
||||||
const scriptsDB = new ScriptsDB().getInstance()
|
|
||||||
|
|
||||||
let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => {
|
|
||||||
return { ...item, id: randomStr(10), index: '--', description: item.description + '|内置脚本' }
|
|
||||||
})
|
|
||||||
|
|
||||||
async function getScriptList({ res }) {
|
async function getScriptList({ res }) {
|
||||||
let data = await scriptsDB.findAsync({})
|
let data = await readScriptList()
|
||||||
data = data.map(item => {
|
data = data.map(item => {
|
||||||
return { ...item, id: item._id }
|
return { ...item, id: item._id }
|
||||||
})
|
})
|
||||||
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
data.push(...localShell)
|
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLocalScriptList({ res }) {
|
|
||||||
res.success({ data: localShell })
|
|
||||||
}
|
|
||||||
|
|
||||||
const addScript = async ({ res, request }) => {
|
const addScript = async ({ res, request }) => {
|
||||||
let { body: { name, description, command, index } } = request
|
let { body: { name, remark, content, index } } = request
|
||||||
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
|
if (!name || !content) return res.fail({ data: false, msg: '参数错误' })
|
||||||
index = Number(index) || 0
|
index = Number(index) || 0
|
||||||
let record = { name, description, command, index }
|
let scriptsList = await readScriptList()
|
||||||
await scriptsDB.insertAsync(record)
|
let record = { name, remark, content, index }
|
||||||
|
scriptsList.push(record)
|
||||||
|
await writeScriptList(scriptsList)
|
||||||
res.success({ data: '添加成功' })
|
res.success({ data: '添加成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScriptList = async ({ res, request }) => {
|
const updateScriptList = async ({ res, request }) => {
|
||||||
let { params: { id } } = request
|
let { params: { id } } = request
|
||||||
let { body: { name, description, command, index } } = request
|
let { body: { name, remark, content, index } } = request
|
||||||
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
|
if (!name || !content) return res.fail({ data: false, msg: '参数错误' })
|
||||||
await scriptsDB.updateAsync({ _id: id }, { name, description, command, index })
|
let scriptsList = await readScriptList()
|
||||||
|
let idx = scriptsList.findIndex(item => item._id === id)
|
||||||
|
if (idx === -1) return res.fail({ data: false, msg: `脚本ID${ id }不存在` })
|
||||||
|
const { _id } = scriptsList[idx]
|
||||||
|
let record = Object.assign({ _id }, { name, remark, content, index })
|
||||||
|
scriptsList.splice(idx, 1, record)
|
||||||
|
await writeScriptList(scriptsList)
|
||||||
res.success({ data: '修改成功' })
|
res.success({ data: '修改成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeScript = async ({ res, request }) => {
|
const removeScript = async ({ res, request }) => {
|
||||||
let { params: { id } } = request
|
let { params: { id } } = request
|
||||||
await scriptsDB.removeAsync({ _id: id })
|
let scriptsList = await readScriptList()
|
||||||
|
let idx = scriptsList.findIndex(item => item._id === id)
|
||||||
|
if (idx === -1) return res.fail({ msg: '脚本ID不存在' })
|
||||||
|
scriptsList.splice(idx, 1)
|
||||||
|
await writeScriptList(scriptsList)
|
||||||
res.success({ data: '移除成功' })
|
res.success({ data: '移除成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchRemoveScript = async ({ res, request }) => {
|
|
||||||
let { body: { ids } } = request
|
|
||||||
if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' })
|
|
||||||
const numRemoved = await scriptsDB.removeAsync({ _id: { $in: ids } }, { multi: true })
|
|
||||||
res.success({ data: `批量移除成功,数量: ${ numRemoved }` })
|
|
||||||
}
|
|
||||||
|
|
||||||
const importScript = async ({ res, request }) => {
|
|
||||||
let { impScript } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
|
||||||
if (impScript) {
|
|
||||||
await impScript({ res, request })
|
|
||||||
} else {
|
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能!' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addScript,
|
addScript,
|
||||||
getScriptList,
|
getScriptList,
|
||||||
getLocalScriptList,
|
|
||||||
updateScriptList,
|
updateScriptList,
|
||||||
removeScript,
|
removeScript
|
||||||
batchRemoveScript,
|
|
||||||
importScript
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
const path = require('path')
|
const { readSSHRecord, writeSSHRecord, readHostList, writeHostList, RSADecryptSync, AESEncryptSync, AESDecryptSync } = require('../utils')
|
||||||
const { RSADecryptAsync, AESEncryptAsync, AESDecryptAsync } = require('../utils/encrypt')
|
|
||||||
const { HostListDB, CredentialsDB } = require('../utils/db-class')
|
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
const credentialsDB = new CredentialsDB().getInstance()
|
|
||||||
|
|
||||||
async function getSSHList({ res }) {
|
async function getSSHList({ res }) {
|
||||||
let data = await credentialsDB.findAsync({})
|
// console.log('get-host-list')
|
||||||
|
let data = await readSSHRecord()
|
||||||
data = data?.map(item => {
|
data = data?.map(item => {
|
||||||
const { name, authType, _id: id, date } = item
|
const { name, authType, _id: id, date } = item
|
||||||
return { id, name, authType, privateKey: '', password: '', date }
|
return { id, name, authType, privateKey: '', password: '', date }
|
||||||
@ -18,17 +14,19 @@ async function getSSHList({ res }) {
|
|||||||
const addSSH = async ({ res, request }) => {
|
const addSSH = async ({ res, request }) => {
|
||||||
let { body: { name, authType, password, privateKey, tempKey } } = request
|
let { body: { name, authType, password, privateKey, tempKey } } = request
|
||||||
let record = { name, authType, password, privateKey }
|
let record = { name, authType, password, privateKey }
|
||||||
if (!name || !record[authType]) return res.fail({ data: false, msg: '参数错误' })
|
if(!name || !record[authType]) return res.fail({ data: false, msg: '参数错误' })
|
||||||
let count = await credentialsDB.countAsync({ name })
|
let sshRecord = await readSSHRecord()
|
||||||
if (count > 0) return res.fail({ data: false, msg: '已存在同名凭证' })
|
if (sshRecord.some(item => item.name === name)) return res.fail({ data: false, msg: '已存在同名凭证' })
|
||||||
|
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
console.log('clearTempKey:', clearTempKey)
|
console.log('clearTempKey:', clearTempKey)
|
||||||
const clearSSHKey = await AESDecryptAsync(record[authType], clearTempKey)
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
record[authType] = await AESEncryptAsync(clearSSHKey)
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
// console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
await credentialsDB.insertAsync({ ...record, date: Date.now() })
|
|
||||||
|
sshRecord.push({ ...record, date: Date.now() })
|
||||||
|
await writeSSHRecord(sshRecord)
|
||||||
consola.info('添加凭证:', name)
|
consola.info('添加凭证:', name)
|
||||||
res.success({ data: '保存成功' })
|
res.success({ data: '保存成功' })
|
||||||
}
|
}
|
||||||
@ -36,67 +34,59 @@ const addSSH = async ({ res, request }) => {
|
|||||||
const updateSSH = async ({ res, request }) => {
|
const updateSSH = async ({ res, request }) => {
|
||||||
let { body: { id, name, authType, password, privateKey, date, tempKey } } = request
|
let { body: { id, name, authType, password, privateKey, date, tempKey } } = request
|
||||||
let record = { name, authType, password, privateKey, date }
|
let record = { name, authType, password, privateKey, date }
|
||||||
if (!id || !name) return res.fail({ data: false, msg: '请输入凭据名称' })
|
if(!id || !name) return res.fail({ data: false, msg: '请输入凭据名称' })
|
||||||
let oldRecord = await credentialsDB.findOneAsync({ _id: id })
|
let sshRecord = await readSSHRecord()
|
||||||
if (!oldRecord) return res.fail({ data: false, msg: '凭证不存在' })
|
let idx = sshRecord.findIndex(item => item._id === id)
|
||||||
|
if (sshRecord.some(item => item.name === name && item.date !== date)) return res.fail({ data: false, msg: '已存在同名凭证' })
|
||||||
|
if(idx === -1) res.fail({ data: false, msg: '请输入凭据名称' })
|
||||||
|
const oldRecord = sshRecord[idx]
|
||||||
// 判断原记录是否存在当前更新记录的认证方式
|
// 判断原记录是否存在当前更新记录的认证方式
|
||||||
if (!oldRecord[authType] && !record[authType]) return res.fail({ data: false, msg: `请输入${ authType === 'password' ? '密码' : '密钥' }` })
|
if (!oldRecord[authType] && !record[authType]) return res.fail({ data: false, msg: `请输入${ authType === 'password' ? '密码' : '密钥' }` })
|
||||||
if (!record[authType] && oldRecord[authType]) {
|
if (!record[authType] && oldRecord[authType]) {
|
||||||
record[authType] = oldRecord[authType]
|
record[authType] = oldRecord[authType]
|
||||||
} else {
|
} else {
|
||||||
const clearTempKey = await RSADecryptAsync(tempKey)
|
const clearTempKey = await RSADecryptSync(tempKey)
|
||||||
console.log('clearTempKey:', clearTempKey)
|
console.log('clearTempKey:', clearTempKey)
|
||||||
const clearSSHKey = await AESDecryptAsync(record[authType], clearTempKey)
|
const clearSSHKey = await AESDecryptSync(record[authType], clearTempKey)
|
||||||
// console.log(`${ authType }原密文: `, clearSSHKey)
|
// console.log(`${ authType }原密文: `, clearSSHKey)
|
||||||
record[authType] = await AESEncryptAsync(clearSSHKey)
|
record[authType] = await AESEncryptSync(clearSSHKey)
|
||||||
// console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
console.log(`${ authType }__commonKey加密存储: `, record[authType])
|
||||||
}
|
}
|
||||||
await credentialsDB.updateAsync({ _id: id }, record)
|
record._id = sshRecord[idx]._id
|
||||||
|
sshRecord.splice(idx, 1, record)
|
||||||
|
await writeSSHRecord(sshRecord)
|
||||||
consola.info('修改凭证:', name)
|
consola.info('修改凭证:', name)
|
||||||
res.success({ data: '保存成功' })
|
res.success({ data: '保存成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSSH = async ({ res, request }) => {
|
const removeSSH = async ({ res, request }) => {
|
||||||
let { params: { id } } = request
|
let { params: { id } } = request
|
||||||
let count = await credentialsDB.countAsync({ _id: id })
|
let sshRecord = await readSSHRecord()
|
||||||
if (count === 0) return res.fail({ msg: '凭证不存在' })
|
let idx = sshRecord.findIndex(item => item._id === id)
|
||||||
|
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
||||||
|
sshRecord.splice(idx, 1)
|
||||||
// 将删除的凭证id从host中删除
|
// 将删除的凭证id从host中删除
|
||||||
let hostList = await hostListDB.findAsync({})
|
let hostList = await readHostList()
|
||||||
if (Array.isArray(hostList) && hostList.length > 0) {
|
hostList = hostList.map(item => {
|
||||||
for (let host of hostList) {
|
if (item.credential === id) item.credential = ''
|
||||||
let { credential } = host
|
return item
|
||||||
credential = await AESDecryptAsync(credential)
|
})
|
||||||
if (credential === id) {
|
await writeHostList(hostList)
|
||||||
host.credential = ''
|
|
||||||
await hostListDB.updateAsync({ _id: host._id }, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await hostListDB.compactDatafileAsync()
|
|
||||||
consola.info('移除凭证:', id)
|
consola.info('移除凭证:', id)
|
||||||
await credentialsDB.removeAsync({ _id: id })
|
await writeSSHRecord(sshRecord)
|
||||||
res.success({ data: '移除成功' })
|
res.success({ data: '移除成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCommand = async ({ res, request }) => {
|
const getCommand = async ({ res, request }) => {
|
||||||
let { hostId } = request.query
|
let { host } = request.query
|
||||||
if (!hostId) return res.fail({ data: false, msg: '参数错误' })
|
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
||||||
let hostInfo = await hostListDB.findAsync({})
|
let hostInfo = await readHostList()
|
||||||
let record = hostInfo?.find(item => item._id === hostId)
|
let record = hostInfo?.find(item => item.host === host)
|
||||||
consola.info('查询登录后执行的指令:', hostId)
|
consola.info('查询登录后执行的指令:', host)
|
||||||
if (!record) return res.fail({ data: false, msg: 'host not found' })
|
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 })
|
if(!command) return res.success({ data: false }) // command不存在
|
||||||
res.success({ data: command })
|
res.success({ data: command }) // 存在
|
||||||
}
|
|
||||||
|
|
||||||
const decryptPrivateKey = async ({ res, request }) => {
|
|
||||||
let { dePrivateKey } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
|
||||||
if (dePrivateKey) {
|
|
||||||
await dePrivateKey({ res, request })
|
|
||||||
} else {
|
|
||||||
return res.fail({ data: false, msg: 'Plus专属功能,无法解密私钥!' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -104,6 +94,5 @@ module.exports = {
|
|||||||
addSSH,
|
addSSH,
|
||||||
updateSSH,
|
updateSSH,
|
||||||
removeSSH,
|
removeSSH,
|
||||||
getCommand,
|
getCommand
|
||||||
decryptPrivateKey
|
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,8 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
const axios = require('axios')
|
const { getNetIPInfo, readKey, writeKey, RSADecryptSync, AESEncryptSync, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils')
|
||||||
const speakeasy = require('speakeasy')
|
|
||||||
const QRCode = require('qrcode')
|
|
||||||
const version = require('../../package.json').version
|
|
||||||
const getLicenseInfo = require('../utils/get-plus')
|
|
||||||
const { plusServer1, plusServer2 } = require('../utils/plus-server')
|
|
||||||
const { sendNoticeAsync } = require('../utils/notify')
|
|
||||||
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
|
|
||||||
const { getNetIPInfo } = require('../utils/tools')
|
|
||||||
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
|
|
||||||
|
|
||||||
const keyDB = new KeyDB().getInstance()
|
|
||||||
const logDB = new LogDB().getInstance()
|
|
||||||
const plusDB = new PlusDB().getInstance()
|
|
||||||
|
|
||||||
const getpublicKey = async ({ res }) => {
|
const getpublicKey = async ({ res }) => {
|
||||||
let { publicKey: data } = await keyDB.findOneAsync({})
|
let { publicKey: data } = await readKey()
|
||||||
if (!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
if (!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
@ -29,15 +16,16 @@ let loginCountDown = forbidTimer
|
|||||||
let forbidLogin = false
|
let forbidLogin = false
|
||||||
|
|
||||||
const login = async ({ res, request }) => {
|
const login = async ({ res, request }) => {
|
||||||
let { body: { loginName, ciphertext, jwtExpires, mfa2Token }, ip: clientIp } = request
|
let { body: { loginName, ciphertext, jwtExpires }, ip: clientIp } = request
|
||||||
if (!loginName && !ciphertext) return res.fail({ msg: '请求非法!' })
|
if (!loginName && !ciphertext) return res.fail({ msg: '请求非法!' })
|
||||||
if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
||||||
loginErrCount++
|
loginErrCount++
|
||||||
loginErrTotal++
|
loginErrTotal++
|
||||||
if (loginErrCount >= allowErrCount) {
|
if (loginErrCount >= allowErrCount) {
|
||||||
const { ip, country, city } = await getNetIPInfo(clientIp)
|
const { ip, country, city } = await getNetIPInfo(clientIp)
|
||||||
// 异步发送通知&禁止登录
|
// 发送通知&禁止登录
|
||||||
sendNoticeAsync('err_login', '登录错误提醒', `错误登录次数: ${ loginErrTotal }\n地点:${ country + city }\nIP: ${ ip }`)
|
let sw = getNotifySwByType('err_login')
|
||||||
|
if (sw) sendEmailToConfList('登录错误提醒', `重新登录次数: ${ loginErrTotal }<br/>地点:${ country + city }<br/>IP: ${ ip }`)
|
||||||
forbidLogin = true
|
forbidLogin = true
|
||||||
loginErrCount = 0
|
loginErrCount = 0
|
||||||
|
|
||||||
@ -60,13 +48,10 @@ const login = async ({ res, request }) => {
|
|||||||
|
|
||||||
// 登录流程
|
// 登录流程
|
||||||
try {
|
try {
|
||||||
let loginPwd = await RSADecryptAsync(ciphertext)
|
// console.log('ciphertext', ciphertext)
|
||||||
let { user, pwd, enableMFA2, secret } = await keyDB.findOneAsync({})
|
let loginPwd = await RSADecryptSync(ciphertext)
|
||||||
if (enableMFA2) {
|
// console.log('Decrypt解密password:', loginPwd)
|
||||||
const isValid = speakeasy.totp.verify({ secret, encoding: 'base32', token: mfa2Token, window: 1 })
|
let { user, pwd } = await readKey()
|
||||||
console.log('MFA2 verfify:', isValid)
|
|
||||||
if (!isValid) return res.fail({ msg: '验证失败' })
|
|
||||||
}
|
|
||||||
if (loginName === user && loginPwd === 'admin' && pwd === 'admin') {
|
if (loginName === user && loginPwd === 'admin' && pwd === 'admin') {
|
||||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认用户名和密码' })
|
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认用户名和密码' })
|
||||||
@ -76,8 +61,8 @@ const login = async ({ res, request }) => {
|
|||||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
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.message)
|
console.log('解密失败:', error)
|
||||||
res.fail({ msg: '登录失败, 请查看服务端日志' })
|
res.fail({ msg: '解密失败, 请查看服务端日志' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,141 +71,50 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
|||||||
|
|
||||||
// consola.success('登录成功, 准备生成token', new Date())
|
// consola.success('登录成功, 准备生成token', new Date())
|
||||||
// 生产token
|
// 生产token
|
||||||
let { commonKey } = await keyDB.findOneAsync({})
|
let { commonKey } = await readKey()
|
||||||
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
||||||
token = await AESEncryptAsync(token) // 对称加密token后再传输给前端
|
token = await AESEncryptSync(token) // 对称加密token后再传输给前端
|
||||||
|
|
||||||
// 记录客户端登录IP(用于判断是否异地且只保留最近10<EFBFBD><EFBFBD>)
|
// 记录客户端登录IP(用于判断是否异地且只保留最近10条)
|
||||||
const clientIPInfo = await getNetIPInfo(clientIp)
|
const clientIPInfo = await getNetIPInfo(clientIp)
|
||||||
const { ip, country, city } = clientIPInfo || {}
|
const { ip, country, city } = clientIPInfo || {}
|
||||||
consola.info('登录成功:', new Date(), { ip, country, city })
|
consola.info('登录成功:', new Date(), { ip, country, city })
|
||||||
|
|
||||||
// 登录通知
|
// 邮件登录通知
|
||||||
sendNoticeAsync('login', '登录提醒', `地点:${ country + city }\nIP: ${ ip }`)
|
let sw = getNotifySwByType('login')
|
||||||
|
if (sw) sendEmailToConfList('登录提醒', `地点:${ country + city }<br/>IP: ${ ip }`)
|
||||||
|
|
||||||
await logDB.insertAsync({ ip, country, city, date: Date.now(), type: 'login' })
|
global.loginRecord.unshift(clientIPInfo)
|
||||||
|
if (global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePwd = async ({ res, request }) => {
|
const updatePwd = async ({ res, request }) => {
|
||||||
let { body: { oldLoginName, oldPwd, newLoginName, newPwd } } = request
|
let { body: { oldLoginName, oldPwd, newLoginName, newPwd } } = request
|
||||||
let rsaOldPwd = await RSADecryptAsync(oldPwd)
|
let rsaOldPwd = await RSADecryptSync(oldPwd)
|
||||||
oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd)
|
oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd)
|
||||||
let keyObj = await keyDB.findOneAsync({})
|
let keyObj = await readKey()
|
||||||
let { user, pwd } = keyObj
|
let { user, pwd } = keyObj
|
||||||
if (oldLoginName !== user || oldPwd !== pwd) return res.fail({ data: false, msg: '原用户名或密码校验失败' })
|
if (oldLoginName !== user || oldPwd !== pwd) return res.fail({ data: false, msg: '原用户名或密码校验失败' })
|
||||||
// 旧密钥校验通过,加密保存新密码
|
// 旧密钥校验通过,加密保存新密码
|
||||||
newPwd = await RSADecryptAsync(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(await RSADecryptAsync(newPwd))
|
newPwd = await RSADecryptSync(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(await RSADecryptSync(newPwd))
|
||||||
keyObj.user = newLoginName
|
keyObj.user = newLoginName
|
||||||
keyObj.pwd = newPwd
|
keyObj.pwd = newPwd
|
||||||
await keyDB.updateAsync({}, keyObj)
|
await writeKey(keyObj)
|
||||||
sendNoticeAsync('updatePwd', '用户密码修改提醒', `原用户名:${ user }\n更新用户名: ${ newLoginName }`)
|
|
||||||
|
let sw = getNotifySwByType('updatePwd')
|
||||||
|
if (sw) sendEmailToConfList(`登录信息修改提醒, 新用户名: ${ newLoginName }`)
|
||||||
|
|
||||||
res.success({ data: true, msg: 'success' })
|
res.success({ data: true, msg: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEasynodeVersion = async ({ res }) => {
|
const getLoginRecord = async ({ res }) => {
|
||||||
try {
|
res.success({ data: global.loginRecord, msg: 'success' })
|
||||||
// const { data } = await axios.get('https://api.github.com/repos/chaos-zhu/easynode/releases/latest')
|
|
||||||
const { data } = await axios.get('https://get-easynode-latest-version.chaoszhu.workers.dev/version')
|
|
||||||
res.success({ data, msg: 'success' })
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('Failed to fetch Easynode latest version:', error)
|
|
||||||
res.fail({ msg: 'Failed to fetch Easynode latest version' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tempSecret = null
|
|
||||||
const getMFA2Status = async ({ res }) => {
|
|
||||||
const { enableMFA2 = false } = await keyDB.findOneAsync({})
|
|
||||||
res.success({ data: enableMFA2, msg: 'success' })
|
|
||||||
}
|
|
||||||
const getMFA2Code = async ({ res }) => {
|
|
||||||
const { user } = await keyDB.findOneAsync({})
|
|
||||||
let { otpauth_url, base32 } = speakeasy.generateSecret({ name: `EasyNode-${ user }`, length: 20 })
|
|
||||||
tempSecret = base32
|
|
||||||
const qrImage = await QRCode.toDataURL(otpauth_url)
|
|
||||||
const data = { qrImage, secret: tempSecret }
|
|
||||||
res.success({ data, msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableMFA2 = async ({ res, request }) => {
|
|
||||||
const { body: { token } } = request
|
|
||||||
if (!token) return res.fail({ data: false, msg: '参数错误' })
|
|
||||||
try {
|
|
||||||
// const isValid = authenticator.verify({ token, secret: tempSecret })
|
|
||||||
const isValid = speakeasy.totp.verify({ secret: tempSecret, encoding: 'base32', token, window: 1 })
|
|
||||||
if (!isValid) return res.fail({ msg: '验证失败' })
|
|
||||||
const keyConfig = await keyDB.findOneAsync({})
|
|
||||||
keyConfig.enableMFA2 = true
|
|
||||||
keyConfig.secret = tempSecret
|
|
||||||
tempSecret = null
|
|
||||||
await keyDB.updateAsync({}, keyConfig)
|
|
||||||
res.success({ msg: '验证成功' })
|
|
||||||
} catch (error) {
|
|
||||||
res.fail({ msg: `验证失败: ${ error.message }` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const disableMFA2 = async ({ res }) => {
|
|
||||||
const keyConfig = await keyDB.findOneAsync({})
|
|
||||||
keyConfig.enableMFA2 = false
|
|
||||||
keyConfig.secret = null
|
|
||||||
await keyDB.updateAsync({}, keyConfig)
|
|
||||||
res.success({ msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusInfo = async ({ res }) => {
|
|
||||||
let data = await plusDB.findOneAsync({})
|
|
||||||
delete data?._id
|
|
||||||
delete data?.decryptKey
|
|
||||||
res.success({ data, msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusDiscount = async ({ res } = {}) => {
|
|
||||||
if (process.env.EXEC_ENV === 'local') return res.success({ discount: false })
|
|
||||||
const servers = [plusServer1, plusServer2]
|
|
||||||
for (const server of servers) {
|
|
||||||
try {
|
|
||||||
const url = `${ server }/api/announcement/public?version=${ version }`
|
|
||||||
const response = await fetch(url)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${ response.status }`)
|
|
||||||
}
|
|
||||||
const data = await response.json()
|
|
||||||
return res.success({ data, msg: 'success' })
|
|
||||||
} catch (error) {
|
|
||||||
if (server === servers[servers.length - 1]) {
|
|
||||||
consola.error('All servers failed:', error.message)
|
|
||||||
return res.success({ discount: false })
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlusConf = async ({ res }) => {
|
|
||||||
const { key } = await plusDB.findOneAsync({}) || {}
|
|
||||||
res.success({ data: key || '', msg: 'success' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlusKey = async ({ res, request }) => {
|
|
||||||
const { body: { key } } = request
|
|
||||||
const { success, msg } = await getLicenseInfo(key)
|
|
||||||
if (!success) return res.fail({ msg })
|
|
||||||
res.success({ msg: 'success' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
login,
|
login,
|
||||||
getpublicKey,
|
getpublicKey,
|
||||||
updatePwd,
|
updatePwd,
|
||||||
getEasynodeVersion,
|
getLoginRecord
|
||||||
getMFA2Status,
|
|
||||||
getMFA2Code,
|
|
||||||
enableMFA2,
|
|
||||||
disableMFA2,
|
|
||||||
getPlusInfo,
|
|
||||||
getPlusDiscount,
|
|
||||||
getPlusConf,
|
|
||||||
updatePlusKey
|
|
||||||
}
|
}
|
||||||
|
229
server/app/db.js
@ -1,104 +1,183 @@
|
|||||||
const NodeRSA = require('node-rsa')
|
const { writeKey, writeNotifyList, writeGroupList } = require('./utils/storage')
|
||||||
const { randomStr } = require('./utils/tools')
|
const { KeyDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./utils/db-class')
|
||||||
const { AESEncryptAsync } = require('./utils/encrypt')
|
const { readScriptList, writeScriptList } = require('./utils')
|
||||||
const { KeyDB, GroupDB, NotifyDB, NotifyConfigDB } = require('./utils/db-class')
|
|
||||||
|
|
||||||
async function initKeyDB() {
|
function initKeyDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const keyDB = new KeyDB().getInstance()
|
const keyDB = new KeyDB().getInstance()
|
||||||
let count = await keyDB.countAsync({})
|
keyDB.count({}, async (err, count) => {
|
||||||
if (count !== 0) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
if (err) {
|
||||||
let newConfig = {
|
consola.log('初始化keyDB错误:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
if (count === 0) {
|
||||||
|
consola.log('初始化keyDB✔')
|
||||||
|
const defaultData = {
|
||||||
user: 'admin',
|
user: 'admin',
|
||||||
pwd: 'admin',
|
pwd: 'admin',
|
||||||
commonKey: randomStr(16),
|
commonKey: '',
|
||||||
publicKey: '',
|
publicKey: '',
|
||||||
privateKey: ''
|
privateKey: ''
|
||||||
}
|
}
|
||||||
await keyDB.insertAsync(newConfig)
|
await writeKey(defaultData)
|
||||||
let key = new NodeRSA({ b: 1024 })
|
}
|
||||||
key.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' })
|
}
|
||||||
let privateKey = key.exportKey('pkcs1-private-pem')
|
resolve()
|
||||||
let publicKey = key.exportKey('pkcs8-public-pem')
|
})
|
||||||
newConfig.privateKey = await AESEncryptAsync(privateKey, newConfig.commonKey) // 加密私钥
|
})
|
||||||
newConfig.publicKey = publicKey // 公开公钥
|
|
||||||
await keyDB.updateAsync({}, { $set: newConfig }, { upsert: true })
|
|
||||||
consola.info('Task: 已生成新的非对称加密公私钥')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initGroupDB() {
|
function initNotifyDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const notifyDB = new NotifyDB().getInstance()
|
||||||
|
notifyDB.count({}, async (err, count) => {
|
||||||
|
if (err) {
|
||||||
|
consola.log('初始化notifyDB错误:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
if (count === 0) {
|
||||||
|
consola.log('初始化notifyDB✔')
|
||||||
|
const defaultData = [{
|
||||||
|
'type': 'login',
|
||||||
|
'desc': '登录面板提醒',
|
||||||
|
'sw': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'err_login',
|
||||||
|
'desc': '登录错误提醒(连续5次)',
|
||||||
|
'sw': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'updatePwd',
|
||||||
|
'desc': '修改密码提醒',
|
||||||
|
'sw': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'host_offline',
|
||||||
|
'desc': '客户端离线提醒(每小时最多发送一次提醒)',
|
||||||
|
'sw': true
|
||||||
|
}]
|
||||||
|
await writeNotifyList(defaultData)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initGroupDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const groupDB = new GroupDB().getInstance()
|
const groupDB = new GroupDB().getInstance()
|
||||||
let count = await groupDB.countAsync({})
|
groupDB.count({}, async (err, count) => {
|
||||||
|
if (err) {
|
||||||
|
consola.log('初始化groupDB错误:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
consola.log('初始化groupDB✔')
|
consola.log('初始化groupDB✔')
|
||||||
const defaultData = [{ '_id': 'default', 'name': '默认分组', 'index': 0 }]
|
const defaultData = [{ '_id': 'default', 'name': '默认分组', 'index': 0 }]
|
||||||
return groupDB.insertAsync(defaultData)
|
await writeGroupList(defaultData)
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initNotifyDB() {
|
function initEmailNotifyDB() {
|
||||||
const notifyDB = new NotifyDB().getInstance()
|
return new Promise((resolve, reject) => {
|
||||||
let count = await notifyDB.countAsync({})
|
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||||
if (count !== 0) return
|
emailNotifyDB.count({}, async (err, count) => {
|
||||||
consola.log('初始化notifyDB✔')
|
if (err) {
|
||||||
let defaultData = [{
|
consola.log('初始化emailNotifyDB错误:', err)
|
||||||
'type': 'login',
|
reject(err)
|
||||||
'desc': '登录面板提醒',
|
} else {
|
||||||
'sw': false
|
if (count === 0) {
|
||||||
}, {
|
consola.log('初始化emailNotifyDB✔')
|
||||||
'type': 'err_login',
|
|
||||||
'desc': '登录错误提醒(连续5次)',
|
|
||||||
'sw': false
|
|
||||||
}, {
|
|
||||||
'type': 'updatePwd',
|
|
||||||
'desc': '修改密码提醒',
|
|
||||||
'sw': false
|
|
||||||
}, {
|
|
||||||
'type': 'host_login',
|
|
||||||
'desc': '服务器登录提醒',
|
|
||||||
'sw': false
|
|
||||||
}, {
|
|
||||||
'type': 'onekey_complete',
|
|
||||||
'desc': '批量指令执行完成提醒',
|
|
||||||
'sw': false
|
|
||||||
}, {
|
|
||||||
'type': 'host_expired',
|
|
||||||
'desc': '服务器到期提醒',
|
|
||||||
'sw': false
|
|
||||||
}]
|
|
||||||
return notifyDB.insertAsync(defaultData)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initNotifyConfigDB() {
|
|
||||||
const notifyConfigDB = new NotifyConfigDB().getInstance()
|
|
||||||
let notifyConfig = await notifyConfigDB.findOneAsync({})
|
|
||||||
consola.log('初始化NotifyConfigDB✔')
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
type: 'sct',
|
'support': [
|
||||||
sct: {
|
{
|
||||||
sendKey: ''
|
'name': 'QQ邮箱',
|
||||||
|
'target': 'qq',
|
||||||
|
'host': 'smtp.qq.com',
|
||||||
|
'port': 465,
|
||||||
|
'secure': true,
|
||||||
|
'tls': {
|
||||||
|
'rejectUnauthorized': false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
email: {
|
{
|
||||||
service: 'QQ',
|
'name': '网易126',
|
||||||
user: '',
|
'target': 'wangyi126',
|
||||||
pass: ''
|
'host': 'smtp.126.com',
|
||||||
|
'port': 465,
|
||||||
|
'secure': true,
|
||||||
|
'tls': {
|
||||||
|
'rejectUnauthorized': false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tg: {
|
{
|
||||||
token: '',
|
'name': '网易163',
|
||||||
chatId: ''
|
'target': 'wangyi163',
|
||||||
|
'host': 'smtp.163.com',
|
||||||
|
'port': 465,
|
||||||
|
'secure': true,
|
||||||
|
'tls': {
|
||||||
|
'rejectUnauthorized': false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (notifyConfig) {
|
],
|
||||||
await notifyConfigDB.removeAsync({ _id: notifyConfig._id })
|
'user': [
|
||||||
delete notifyConfig._id
|
]
|
||||||
return notifyConfigDB.insertAsync(Object.assign({}, defaultData, notifyConfig))
|
|
||||||
}
|
}
|
||||||
return notifyConfigDB.insertAsync(defaultData)
|
emailNotifyDB.update({}, { $set: defaultData }, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
emailNotifyDB.compactDatafile()
|
||||||
|
resolve(numReplaced)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initScriptsDB() {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
let scriptList = await readScriptList()
|
||||||
|
let clientInstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash'
|
||||||
|
let clientUninstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash'
|
||||||
|
let clientVersion = process.env.CLIENT_VERSION
|
||||||
|
consola.info('客户端版本:', clientVersion)
|
||||||
|
let installId = `clientInstall${ clientVersion }`
|
||||||
|
let uninstallId = `clientUninstall${ clientVersion }`
|
||||||
|
|
||||||
|
let isClientInstall = scriptList?.find(script => script._id = installId)
|
||||||
|
let isClientUninstall = scriptList?.find(script => script._id = uninstallId)
|
||||||
|
let writeFlag = false
|
||||||
|
if (!isClientInstall) {
|
||||||
|
scriptList.push({ _id: installId, name: `easynode-client-${ clientVersion }安装脚本`, remark: '系统内置|重启生成', content: clientInstallScript, index: 99 })
|
||||||
|
writeFlag = true
|
||||||
|
}
|
||||||
|
if (!isClientUninstall) {
|
||||||
|
scriptList.push({ _id: uninstallId, name: `easynode-client-${ clientVersion }卸载脚本`, remark: '系统内置|重启生成', content: clientUninstallScript, index: 98 })
|
||||||
|
writeFlag = true
|
||||||
|
}
|
||||||
|
if (writeFlag) await writeScriptList(scriptList)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
await initKeyDB()
|
await initKeyDB()
|
||||||
await initNotifyDB()
|
await initNotifyDB()
|
||||||
await initGroupDB()
|
await initGroupDB()
|
||||||
await initNotifyConfigDB()
|
await initEmailNotifyDB()
|
||||||
|
await initScriptsDB()
|
||||||
}
|
}
|
40
server/app/db/README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
db目录,初始化后自动生成
|
||||||
|
|
||||||
|
**host.db**
|
||||||
|
|
||||||
|
> 存储服务器基本信息
|
||||||
|
|
||||||
|
**key.db**
|
||||||
|
|
||||||
|
> 用于加密的密钥相关
|
||||||
|
|
||||||
|
**credentials.db**
|
||||||
|
|
||||||
|
> ssh密钥记录(加密存储)
|
||||||
|
|
||||||
|
**email.db**
|
||||||
|
|
||||||
|
> 邮件配置
|
||||||
|
|
||||||
|
- port: 587 --> secure: false
|
||||||
|
```db
|
||||||
|
// Gmail调试不通过, 暂缓
|
||||||
|
{
|
||||||
|
"name": "Google邮箱",
|
||||||
|
"target": "google",
|
||||||
|
"host": "smtp.gmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"tls": {
|
||||||
|
"rejectUnauthorized": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**notify.db**
|
||||||
|
|
||||||
|
> 通知配置
|
||||||
|
|
||||||
|
**group.db**
|
||||||
|
|
||||||
|
> 服务器分组配置
|
49
server/app/init.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const NodeRSA = require('node-rsa')
|
||||||
|
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncryptSync } = require('./utils')
|
||||||
|
|
||||||
|
const isDev = !isProd()
|
||||||
|
|
||||||
|
// 存储本机IP, 供host列表接口调用
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async function initLocalIp() {
|
||||||
|
if(isDev) return consola.info('非生产环境不初始化保存本地IP')
|
||||||
|
const localNetIPInfo = await getNetIPInfo()
|
||||||
|
let vpsList = await readHostList()
|
||||||
|
let { ip: localNetIP } = localNetIPInfo
|
||||||
|
if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP)
|
||||||
|
vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' })
|
||||||
|
writeHostList(vpsList)
|
||||||
|
consola.info('Task: 生产环境首次启动储存本机IP: ', localNetIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
||||||
|
async function initRsa() {
|
||||||
|
let keyObj = await readKey()
|
||||||
|
if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
||||||
|
let key = new NodeRSA({ b: 1024 })
|
||||||
|
key.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' })
|
||||||
|
let privateKey = key.exportKey('pkcs1-private-pem')
|
||||||
|
let publicKey = key.exportKey('pkcs8-public-pem')
|
||||||
|
keyObj.privateKey = await AESEncryptSync(privateKey) // 加密私钥
|
||||||
|
keyObj.publicKey = publicKey // 公开公钥
|
||||||
|
await writeKey(keyObj)
|
||||||
|
consola.info('Task: 已生成新的非对称加密公私钥')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机的commonKey secret
|
||||||
|
async function randomJWTSecret() {
|
||||||
|
let keyObj = await readKey()
|
||||||
|
if(keyObj?.commonKey) return consola.info('commonKey密钥已存在')
|
||||||
|
|
||||||
|
keyObj.commonKey = randomStr(16)
|
||||||
|
await writeKey(keyObj)
|
||||||
|
consola.info('Task: 已生成新的随机commonKey密钥')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await randomJWTSecret() // 全局密钥
|
||||||
|
await initRsa() // 全局公钥密钥
|
||||||
|
// initLocalIp() // :TODO: 默认添加服务端vps
|
||||||
|
// 用于记录客户端登录IP的列表
|
||||||
|
global.loginRecord = []
|
||||||
|
}
|
@ -1,13 +1,15 @@
|
|||||||
|
const consola = require('consola')
|
||||||
|
global.consola = consola
|
||||||
const { httpServer } = require('./server')
|
const { httpServer } = require('./server')
|
||||||
const initDB = require('./db')
|
const initDB = require('./db')
|
||||||
|
const initEncryptConf = require('./init')
|
||||||
const scheduleJob = require('./schedule')
|
const scheduleJob = require('./schedule')
|
||||||
const getLicenseInfo = require('./utils/get-plus')
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await initDB()
|
await initDB()
|
||||||
|
await initEncryptConf()
|
||||||
httpServer()
|
httpServer()
|
||||||
scheduleJob()
|
scheduleJob()
|
||||||
getLicenseInfo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
const { verifyAuthSync } = require('../utils')
|
||||||
const { apiPrefix } = require('../config')
|
const { apiPrefix } = require('../config')
|
||||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
|
||||||
|
|
||||||
let whitePath = [
|
let whitePath = [
|
||||||
'/login',
|
'/login',
|
||||||
@ -10,11 +10,11 @@ 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
|
||||||
consola.info('verify path: ', path)
|
consola.info('verify path: ', path)
|
||||||
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
|
||||||
const { code, msg } = await verifyAuthSync(token, request.ip)
|
const { code, msg } = await verifyAuthSync(token, request.ip)
|
||||||
switch (code) {
|
switch(code) {
|
||||||
case 1:
|
case 1:
|
||||||
return await next()
|
return await next()
|
||||||
case -1:
|
case -1:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
const ipFilter = require('./ipFilter') // IP过滤
|
|
||||||
const responseHandler = require('./response') // 统一返回格式, 错误捕获
|
const responseHandler = require('./response') // 统一返回格式, 错误捕获
|
||||||
const useAuth = require('./auth') // 鉴权
|
const useAuth = require('./auth') // 鉴权
|
||||||
// const useCors = require('./cors') // 处理跨域[暂时禁止]
|
// const useCors = require('./cors') // 处理跨域[暂时禁止]
|
||||||
@ -9,8 +8,8 @@ const useStatic = require('./static') // 静态目录
|
|||||||
const compress = require('./compress') // br/gzip压缩
|
const compress = require('./compress') // br/gzip压缩
|
||||||
const history = require('./history') // vue-router的history模式
|
const history = require('./history') // vue-router的history模式
|
||||||
|
|
||||||
|
// 注意注册顺序
|
||||||
module.exports = [
|
module.exports = [
|
||||||
ipFilter,
|
|
||||||
compress,
|
compress,
|
||||||
history,
|
history,
|
||||||
useStatic, // staic先注册,不然会被jwt拦截
|
useStatic, // staic先注册,不然会被jwt拦截
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
// 白名单IP
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const { isAllowedIp } = require('../utils/tools')
|
|
||||||
|
|
||||||
const htmlPath = path.join(__dirname, '../template/ipForbidden.html')
|
|
||||||
const ipForbiddenHtml = fs.readFileSync(htmlPath, 'utf8')
|
|
||||||
|
|
||||||
const ipFilter = async (ctx, next) => {
|
|
||||||
// console.log('requestIP:', ctx.request.ip)
|
|
||||||
if (isAllowedIp(ctx.request.ip)) return await next()
|
|
||||||
ctx.status = 403
|
|
||||||
ctx.body = ipForbiddenHtml
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ipFilter
|
|
@ -3,28 +3,27 @@ const { outDir, recordLog } = require('../config').logConfig
|
|||||||
|
|
||||||
log4js.configure({
|
log4js.configure({
|
||||||
appenders: {
|
appenders: {
|
||||||
console: {
|
// 控制台输出
|
||||||
|
out: {
|
||||||
type: 'stdout',
|
type: 'stdout',
|
||||||
layout: {
|
layout: {
|
||||||
type: 'pattern',
|
type: 'colored'
|
||||||
pattern: '%[%d{yyyy-MM-dd hh:mm:ss.SSS} [%p] -%] %m'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 保存日志文件
|
||||||
cheese: {
|
cheese: {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
maxLogSize: 10 * 1024 * 1024, // unit: bytes 1KB = 1024bytes
|
maxLogSize: 512*1024, // unit: bytes 1KB = 1024bytes
|
||||||
filename: `${ outDir }/receive.log`,
|
filename: `${ outDir }/receive.log`
|
||||||
backups: 10,
|
|
||||||
compress: true,
|
|
||||||
keepFileExt: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
default: {
|
default: {
|
||||||
appenders: ['console', 'cheese'],
|
appenders: [ 'out', 'cheese' ], // 配置
|
||||||
level: 'debug'
|
level: 'info' // 只输出info以上级别的日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// pm2: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const logger = log4js.getLogger()
|
const logger = log4js.getLogger()
|
||||||
@ -57,6 +56,3 @@ const useLog = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = useLog()
|
module.exports = useLog()
|
||||||
|
|
||||||
// 可以先测试一下日志是否正常工作
|
|
||||||
logger.info('日志系统启动')
|
|
@ -5,7 +5,7 @@ const useStatic = koaStatic(staticDir, {
|
|||||||
maxage: 1000 * 60 * 60 * 24 * 30,
|
maxage: 1000 * 60 * 60 * 24 * 30,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
setHeaders: (res, path) => {
|
setHeaders: (res, path) => {
|
||||||
if (path && path.endsWith('.html')) {
|
if(path && path.endsWith('.html')) {
|
||||||
res.setHeader('Cache-Control', 'max-age=0')
|
res.setHeader('Cache-Control', 'max-age=0')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh')
|
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand } = require('../controller/ssh')
|
||||||
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host')
|
const { getHostList, addHost, updateHost, removeHost, importHost } = require('../controller/host')
|
||||||
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount, getPlusConf, updatePlusKey } = require('../controller/user')
|
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
|
||||||
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
|
const { getScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
|
||||||
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
|
||||||
const { getLog } = require('../controller/log')
|
|
||||||
|
|
||||||
const ssh = [
|
const ssh = [
|
||||||
{
|
{
|
||||||
@ -32,11 +30,6 @@ const ssh = [
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/command',
|
path: '/command',
|
||||||
controller: getCommand
|
controller: getCommand
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/decrypt-private-key',
|
|
||||||
controller: decryptPrivateKey
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const host = [
|
const host = [
|
||||||
@ -55,11 +48,6 @@ const host = [
|
|||||||
path: '/host-save',
|
path: '/host-save',
|
||||||
controller: updateHost
|
controller: updateHost
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: 'put',
|
|
||||||
path: '/batch-update-host',
|
|
||||||
controller: batchUpdateHost
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/host-remove',
|
path: '/host-remove',
|
||||||
@ -89,60 +77,35 @@ const user = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/version',
|
path: '/get-login-record',
|
||||||
controller: getEasynodeVersion
|
controller: getLoginRecord
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/mfa2-status',
|
|
||||||
controller: getMFA2Status
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/mfa2-code',
|
|
||||||
controller: getMFA2Code
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/mfa2-enable',
|
|
||||||
controller: enableMFA2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/mfa2-disable',
|
|
||||||
controller: disableMFA2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/plus-info',
|
|
||||||
controller: getPlusInfo
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/plus-discount',
|
|
||||||
controller: getPlusDiscount
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/plus-conf',
|
|
||||||
controller: getPlusConf
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/plus-conf',
|
|
||||||
controller: updatePlusKey
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const notify = [
|
const notify = [
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/notify-config',
|
path: '/support-email',
|
||||||
controller: getNotifyConfig
|
controller: getSupportEmailList
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'get',
|
||||||
path: '/notify-config',
|
path: '/user-email',
|
||||||
controller: updateNotifyConfig
|
controller: getUserEmailList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/user-email',
|
||||||
|
controller: updateUserEmailList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/push-email',
|
||||||
|
controller: pushEmail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'delete',
|
||||||
|
path: '/user-email/:email',
|
||||||
|
controller: removeUserEmail
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -185,11 +148,6 @@ const scripts = [
|
|||||||
path: '/script',
|
path: '/script',
|
||||||
controller: getScriptList
|
controller: getScriptList
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/local-script',
|
|
||||||
controller: getLocalScriptList
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/script',
|
path: '/script',
|
||||||
@ -200,41 +158,11 @@ const scripts = [
|
|||||||
path: '/script/:id',
|
path: '/script/:id',
|
||||||
controller: removeScript
|
controller: removeScript
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/batch-remove-script',
|
|
||||||
controller: batchRemoveScript
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: 'put',
|
method: 'put',
|
||||||
path: '/script/:id',
|
path: '/script/:id',
|
||||||
controller: updateScriptList
|
controller: updateScriptList
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/import-script',
|
|
||||||
controller: importScript
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const onekey = [
|
module.exports = [].concat(ssh, host, user, notify, group, scripts)
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/onekey',
|
|
||||||
controller: getOnekeyRecord
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'post',
|
|
||||||
path: '/onekey',
|
|
||||||
controller: removeOnekeyRecord
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const log = [
|
|
||||||
{
|
|
||||||
method: 'get',
|
|
||||||
path: '/log',
|
|
||||||
controller: getLog
|
|
||||||
}
|
|
||||||
]
|
|
||||||
module.exports = [].concat(ssh, host, user, notify, group, scripts, onekey, log)
|
|
||||||
|
30
server/app/schedule/expired-notify.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const schedule = require('node-schedule')
|
||||||
|
const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils')
|
||||||
|
|
||||||
|
const expiredNotifyJob = async () => {
|
||||||
|
consola.info('=====开始检测服务器到期时间=====', new Date())
|
||||||
|
const hostList = await 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 = () => {
|
||||||
|
// 每天中午12点执行一次。
|
||||||
|
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
|
||||||
|
}
|
@ -1,32 +1,5 @@
|
|||||||
const schedule = require('node-schedule')
|
const expiredNotify = require('./expired-notify')
|
||||||
const { sendNoticeAsync } = require('../utils/notify')
|
|
||||||
const { formatTimestamp } = require('../utils/tools')
|
|
||||||
const { HostListDB } = require('../utils/db-class')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
|
|
||||||
const expiredNotifyJob = async () => {
|
|
||||||
consola.info('=====开始检测服务器到期时间=====', new Date())
|
|
||||||
const hostList = await hostListDB.findAsync({})
|
|
||||||
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 }\nIP: ${ host }\n到期时间:${ formatTimestamp(expired, 'week') }\n控制台: ${ consoleUrl || '未填写' }`
|
|
||||||
if (0 <= restDay && restDay <= 1) {
|
|
||||||
let temp = '有服务器将在一天后到期,请关注\n'
|
|
||||||
sendNoticeAsync('host_expired', title, temp + content)
|
|
||||||
} else if (3 <= restDay && restDay < 4) {
|
|
||||||
let temp = '有服务器将在三天后到期,请关注\n'
|
|
||||||
sendNoticeAsync('host_expired', title, temp + content)
|
|
||||||
} else if (7 <= restDay && restDay < 8) {
|
|
||||||
let temp = '有服务器将在七天后到期,请关注\n'
|
|
||||||
sendNoticeAsync('host_expired', title, temp + content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob)
|
expiredNotify()
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ const { httpPort } = require('./config')
|
|||||||
const middlewares = require('./middlewares')
|
const middlewares = require('./middlewares')
|
||||||
const wsTerminal = require('./socket/terminal')
|
const wsTerminal = require('./socket/terminal')
|
||||||
const wsSftp = require('./socket/sftp')
|
const wsSftp = require('./socket/sftp')
|
||||||
|
const wsHostStatus = require('./socket/host-status')
|
||||||
const wsClientInfo = require('./socket/clients')
|
const wsClientInfo = require('./socket/clients')
|
||||||
const wsOnekey = require('./socket/onekey')
|
const { throwError } = require('./utils')
|
||||||
const { throwError } = require('./utils/tools')
|
|
||||||
|
|
||||||
const httpServer = () => {
|
const httpServer = () => {
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
@ -24,7 +24,7 @@ function serverHandler(app, server) {
|
|||||||
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
||||||
wsTerminal(server) // 终端
|
wsTerminal(server) // 终端
|
||||||
wsSftp(server) // sftp
|
wsSftp(server) // sftp
|
||||||
wsOnekey(server) // 一键指令
|
wsHostStatus(server) // 终端侧边栏host信息
|
||||||
wsClientInfo(server) // 客户端信息
|
wsClientInfo(server) // 客户端信息
|
||||||
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
||||||
app.use(compose(middlewares))
|
app.use(compose(middlewares))
|
||||||
|
@ -1,62 +1,48 @@
|
|||||||
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 { defaultClientPort } = require('../config')
|
const { readHostList } = require('../utils')
|
||||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
const { clientPort } = require('../config')
|
||||||
const { isAllowedIp } = require('../utils/tools')
|
const { verifyAuthSync } = require('../utils')
|
||||||
const { HostListDB } = require('../utils/db-class')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
|
|
||||||
let clientSockets = []
|
let clientSockets = {}, clientsData = {}
|
||||||
let clientsData = {}
|
|
||||||
|
|
||||||
async function getClientsInfo(clientSockets) {
|
async function getClientsInfo(socketId) {
|
||||||
let hostList = await hostListDB.findAsync({})
|
let hostList = await readHostList()
|
||||||
clientSockets.forEach((clientItem) => {
|
|
||||||
// 被删除的客户端断开连接
|
|
||||||
if (!hostList.some(item => item.host === clientItem.host)) clientItem.close && clientItem.close()
|
|
||||||
})
|
|
||||||
hostList
|
hostList
|
||||||
.map(({ host, name, clientPort }) => {
|
?.map(({ host, name }) => {
|
||||||
// 已经建立io连接(无论是否连接成功)的host不再重复建立连接,因为存在多次(reconnectionAttempts)的重试机制
|
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||||
if (clientSockets.some(item => `${ item.host }:${ item.clientPort || defaultClientPort }` === `${ host }:${ clientPort || defaultClientPort }`)) return { name, isIo: true }
|
|
||||||
// console.log(name, 'clientPort:', clientPort)
|
|
||||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort || defaultClientPort }`, {
|
|
||||||
path: '/client/os-info',
|
path: '/client/os-info',
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
reconnectionDelay: 5000,
|
reconnectionDelay: 3000,
|
||||||
reconnectionAttempts: 1000
|
reconnectionAttempts: 3
|
||||||
})
|
})
|
||||||
// 将与客户端连接的socket实例保存起来,web端断开时关闭这些连接
|
// 将与客户端连接的socket实例保存起来,web端断开时关闭这些连接
|
||||||
clientSockets.push({ host, name, clientPort, clientSocket })
|
clientSockets[socketId].push(clientSocket)
|
||||||
return {
|
return {
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
clientPort,
|
|
||||||
clientSocket
|
clientSocket
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.forEach((item) => {
|
.map(({ host, name, clientSocket }) => {
|
||||||
if (item.isIo) return // console.log('已经建立io连接的host不再重复建立连接', item.name)
|
|
||||||
const { host, name, clientPort, clientSocket } = item
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
clientSocket
|
clientSocket
|
||||||
.on('connect', () => {
|
.on('connect', () => {
|
||||||
consola.success('client connect success:', host, name)
|
consola.success('client connect success:', host, name)
|
||||||
clientSocket.on('client_data', (osData) => {
|
clientSocket.on('client_data', (osData) => {
|
||||||
clientsData[`${ host }:${ clientPort || defaultClientPort }`] = { connect: true, ...osData }
|
clientsData[host] = osData
|
||||||
})
|
})
|
||||||
clientSocket.on('client_error', (error) => {
|
clientSocket.on('client_error', (error) => {
|
||||||
clientsData[`${ host }:${ clientPort || defaultClientPort }`] = { connect: true, error: `client_error: ${ error }` }
|
clientsData[host] = error
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on('connect_error', (error) => { // 连接失败
|
.on('connect_error', (error) => {
|
||||||
// consola.error('client connect fail:', host, name, error.message)
|
// consola.error('client connect fail:', host, name, error.message)
|
||||||
clientsData[`${ host }:${ clientPort || defaultClientPort }`] = { connect: false, error: `client_connect_error: ${ error }` }
|
clientsData[host] = null
|
||||||
})
|
})
|
||||||
.on('disconnect', (error) => { // 一方主动断开连接
|
.on('disconnect', () => {
|
||||||
// consola.info('client connect disconnect:', host, name)
|
consola.info('client connect disconnect:', host, name)
|
||||||
clientsData[`${ host }:${ clientPort || defaultClientPort }`] = { connect: false, error: `client_disconnect: ${ error }` }
|
clientsData[host] = null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -71,38 +57,40 @@ module.exports = (httpServer) => {
|
|||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
if (!isAllowedIp(requestIP)) {
|
|
||||||
socket.emit('ip_forbidden', 'IP地址不在白名单中')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
socket.on('init_clients_data', async ({ token }) => {
|
socket.on('init_clients_data', async ({ token }) => {
|
||||||
const { code, msg } = await verifyAuthSync(token, requestIP)
|
// 校验登录态
|
||||||
|
const { code, msg } = await verifyAuthSync(token, clientIp)
|
||||||
if (code !== 1) {
|
if (code !== 1) {
|
||||||
socket.emit('token_verify_fail', msg || '鉴权失败')
|
socket.emit('token_verify_fail', msg || '鉴权失败')
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientsInfo(clientSockets)
|
// 收集web端连接的id
|
||||||
|
clientSockets[socket.id] = []
|
||||||
|
consola.info('client连接socketId: ', socket.id, 'clients-socket已连接数: ', Object.keys(clientSockets).length)
|
||||||
|
|
||||||
socket.on('refresh_clients_data', async () => {
|
// 获取客户端数据
|
||||||
consola.info('refresh clients-socket')
|
getClientsInfo(socket.id)
|
||||||
getClientsInfo(clientSockets)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 立即推送一次
|
||||||
|
socket.emit('clients_data', clientsData)
|
||||||
|
|
||||||
|
// 向web端推送数据
|
||||||
let timer = null
|
let timer = null
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
socket.emit('clients_data', clientsData)
|
socket.emit('clients_data', clientsData)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
|
// 防止内存泄漏
|
||||||
if (timer) clearInterval(timer)
|
if (timer) clearInterval(timer)
|
||||||
clientSockets.forEach(item => item.clientSocket.close && item.clientSocket.close())
|
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||||
clientSockets = []
|
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
||||||
clientsData = {}
|
delete clientSockets[socket.id]
|
||||||
consola.info('clients-socket 连接断开: ', socket.id)
|
consola.info('断开socketId: ', socket.id, 'clients-socket剩余连接数: ', Object.keys(clientSockets).length)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
74
server/app/socket/host-status.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
const { Server: ServerIO } = require('socket.io')
|
||||||
|
const { io: ClientIO } = require('socket.io-client')
|
||||||
|
const { clientPort } = require('../config')
|
||||||
|
const { verifyAuthSync } = require('../utils')
|
||||||
|
|
||||||
|
let hostSockets = {}
|
||||||
|
|
||||||
|
function getHostInfo(serverSocket, host) {
|
||||||
|
let hostSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||||
|
path: '/client/os-info',
|
||||||
|
forceNew: false,
|
||||||
|
timeout: 5000,
|
||||||
|
reconnectionDelay: 3000,
|
||||||
|
reconnectionAttempts: 3
|
||||||
|
})
|
||||||
|
// 将与客户端连接的socket实例保存起来,web端断开时关闭与客户端的连接
|
||||||
|
hostSockets[serverSocket.id] = hostSocket
|
||||||
|
|
||||||
|
hostSocket
|
||||||
|
.on('connect', () => {
|
||||||
|
consola.success('host-status-socket连接成功:', host)
|
||||||
|
hostSocket.on('client_data', (data) => {
|
||||||
|
serverSocket.emit('host_data', data)
|
||||||
|
})
|
||||||
|
hostSocket.on('client_error', () => {
|
||||||
|
serverSocket.emit('host_data', null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on('connect_error', (error) => {
|
||||||
|
consola.error('host-status-socket连接[失败]:', host, error.message)
|
||||||
|
serverSocket.emit('host_data', null)
|
||||||
|
})
|
||||||
|
.on('disconnect', () => {
|
||||||
|
consola.info('host-status-socket连接[断开]:', host)
|
||||||
|
serverSocket.emit('host_data', null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (httpServer) => {
|
||||||
|
const serverIo = new ServerIO(httpServer, {
|
||||||
|
path: '/host-status',
|
||||||
|
cors: {
|
||||||
|
origin: '*' // 需配置跨域
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
serverIo.on('connection', (serverSocket) => {
|
||||||
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
|
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
|
||||||
|
serverSocket.on('init_host_data', async ({ token, host }) => {
|
||||||
|
// 校验登录态
|
||||||
|
const { code, msg } = await verifyAuthSync(token, clientIp)
|
||||||
|
if(code !== 1) {
|
||||||
|
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
|
||||||
|
serverSocket.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户端数据
|
||||||
|
getHostInfo(serverSocket, host)
|
||||||
|
|
||||||
|
consola.info('host-status-socket连接socketId: ', serverSocket.id, 'host-status-socket已连接数: ', Object.keys(hostSockets).length)
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
serverSocket.on('disconnect', () => {
|
||||||
|
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||||
|
let socket = hostSockets[serverSocket.id]
|
||||||
|
socket.close && socket.close()
|
||||||
|
delete hostSockets[serverSocket.id]
|
||||||
|
consola.info('host-status-socket剩余连接数: ', Object.keys(hostSockets).length)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,217 +0,0 @@
|
|||||||
const { Server } = require('socket.io')
|
|
||||||
const { Client: SSHClient } = require('ssh2')
|
|
||||||
const { sendNoticeAsync } = require('../utils/notify')
|
|
||||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
|
||||||
const { shellThrottle } = require('../utils/tools')
|
|
||||||
const { AESDecryptAsync } = require('../utils/encrypt')
|
|
||||||
const { isAllowedIp } = require('../utils/tools')
|
|
||||||
const { HostListDB, CredentialsDB, OnekeyDB } = require('../utils/db-class')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
const credentialsDB = new CredentialsDB().getInstance()
|
|
||||||
const onekeyDB = new OnekeyDB().getInstance()
|
|
||||||
|
|
||||||
const execStatusEnum = {
|
|
||||||
connecting: '连接中',
|
|
||||||
connectFail: '连接失败',
|
|
||||||
executing: '执行中',
|
|
||||||
execSuccess: '执行成功',
|
|
||||||
execFail: '执行失败',
|
|
||||||
execTimeout: '执行超时',
|
|
||||||
socketInterrupt: '执行中断'
|
|
||||||
}
|
|
||||||
|
|
||||||
let isExecuting = false
|
|
||||||
let execResult = []
|
|
||||||
let execClient = []
|
|
||||||
|
|
||||||
function disconnectAllExecClient() {
|
|
||||||
execClient.forEach((sshClient) => {
|
|
||||||
if (sshClient) {
|
|
||||||
sshClient.end()
|
|
||||||
sshClient.destroy()
|
|
||||||
sshClient = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function execShell(socket, sshClient, curRes, resolve) {
|
|
||||||
const throttledDataHandler = shellThrottle(() => {
|
|
||||||
socket.emit('output', execResult)
|
|
||||||
// const memoryUsage = process.memoryUsage()
|
|
||||||
// const formattedMemoryUsage = {
|
|
||||||
// rss: (memoryUsage.rss / 1024 / 1024).toFixed(2) + ' MB', // Resident Set Size: total memory allocated for the process execution
|
|
||||||
// heapTotal: (memoryUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB', // Total size of the allocated heap
|
|
||||||
// heapUsed: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', // Actual memory used during the execution
|
|
||||||
// external: (memoryUsage.external / 1024 / 1024).toFixed(2) + ' MB', // Memory used by "external" components like V8 external memory
|
|
||||||
// arrayBuffers: (memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2) + ' MB' // Memory allocated for ArrayBuffer and SharedArrayBuffer, including all Node.js Buffers
|
|
||||||
// }
|
|
||||||
// console.log(formattedMemoryUsage)
|
|
||||||
}, 500) // 防止内存爆破
|
|
||||||
sshClient.exec(curRes.command, function(err, stream) {
|
|
||||||
if (err) {
|
|
||||||
console.log(curRes.host, '命令执行失败:', err)
|
|
||||||
curRes.status = execStatusEnum.execFail
|
|
||||||
curRes.result += err.toString()
|
|
||||||
socket.emit('output', execResult)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on('close', async () => {
|
|
||||||
// shell关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失
|
|
||||||
await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出
|
|
||||||
// console.log('onekey终端执行完成, 关闭连接: ', curRes.host)
|
|
||||||
if (curRes.status === execStatusEnum.executing) {
|
|
||||||
curRes.status = execStatusEnum.execSuccess
|
|
||||||
}
|
|
||||||
socket.emit('output', execResult)
|
|
||||||
resolve(curRes)
|
|
||||||
sshClient.end()
|
|
||||||
})
|
|
||||||
.on('data', (data) => {
|
|
||||||
// console.log(curRes.host, '执行中: \n' + data)
|
|
||||||
curRes.status = execStatusEnum.executing
|
|
||||||
curRes.result += data.toString()
|
|
||||||
// socket.emit('output', execResult)
|
|
||||||
throttledDataHandler(data)
|
|
||||||
})
|
|
||||||
.stderr
|
|
||||||
.on('data', (data) => {
|
|
||||||
// console.log(curRes.host, '命令执行过程中产生错误: ' + data)
|
|
||||||
curRes.status = execStatusEnum.executing
|
|
||||||
curRes.result += data.toString()
|
|
||||||
// socket.emit('output', execResult)
|
|
||||||
throttledDataHandler(data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
|
||||||
const serverIo = new Server(httpServer, {
|
|
||||||
path: '/onekey',
|
|
||||||
cors: {
|
|
||||||
origin: '*'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
serverIo.on('connection', (socket) => {
|
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
|
||||||
let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
|
||||||
if (!isAllowedIp(requestIP)) {
|
|
||||||
socket.emit('ip_forbidden', 'IP地址不在白名单中')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
consola.success('onekey-terminal websocket 已连接')
|
|
||||||
if (isExecuting) {
|
|
||||||
socket.emit('create_fail', '正在执行中, 请稍后再试')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isExecuting = true
|
|
||||||
socket.on('create', async ({ hostIds, token, command, timeout }) => {
|
|
||||||
const { code } = await verifyAuthSync(token, requestIP)
|
|
||||||
if (code !== 1) {
|
|
||||||
socket.emit('token_verify_fail')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
// 超时未执行完成,强制断开连接
|
|
||||||
const { connecting, executing } = execStatusEnum
|
|
||||||
execResult.forEach(item => {
|
|
||||||
// 连接中和执行中的状态设定为超时
|
|
||||||
if ([connecting, executing].includes(item.status)) {
|
|
||||||
item.status = execStatusEnum.execTimeout
|
|
||||||
}
|
|
||||||
})
|
|
||||||
let reason = `执行超时,已强制终止执行 - 超时时间${ timeout }秒`
|
|
||||||
sendNoticeAsync('onekey_complete', '批量指令执行超时', reason)
|
|
||||||
socket.emit('timeout', { reason, result: execResult })
|
|
||||||
socket.disconnect()
|
|
||||||
disconnectAllExecClient()
|
|
||||||
}, timeout * 1000)
|
|
||||||
console.log('hostIds:', hostIds)
|
|
||||||
// console.log('token:', token)
|
|
||||||
console.log('command:', command)
|
|
||||||
const hostList = await hostListDB.findAsync({})
|
|
||||||
const targetHostsInfo = hostList.filter(item => hostIds.some(id => item._id === id)) || {}
|
|
||||||
// console.log('targetHostsInfo:', targetHostsInfo)
|
|
||||||
if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hostIds }】服务器信息`)
|
|
||||||
// 查找 hostInfo -> 并发执行
|
|
||||||
socket.emit('ready')
|
|
||||||
let execPromise = targetHostsInfo.map((hostInfo, index) => {
|
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
setTimeout(() => reject('执行超时'), timeout * 1000)
|
|
||||||
let { authType, host, port, username } = hostInfo
|
|
||||||
let authInfo = { host, port, username }
|
|
||||||
let curRes = { command, host, port, name: hostInfo.name, result: '', status: execStatusEnum.connecting, date: Date.now() - (targetHostsInfo.length - index) } // , execStatusEnum
|
|
||||||
execResult.push(curRes)
|
|
||||||
try {
|
|
||||||
if (authType === 'credential') {
|
|
||||||
let credentialId = await AESDecryptAsync(hostInfo['credential'])
|
|
||||||
const sshRecordList = await credentialsDB.findAsync({})
|
|
||||||
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
|
||||||
authInfo.authType = sshRecord.authType
|
|
||||||
authInfo[authInfo.authType] = await AESDecryptAsync(sshRecord[authInfo.authType])
|
|
||||||
} else {
|
|
||||||
authInfo[authType] = await AESDecryptAsync(hostInfo[authType])
|
|
||||||
}
|
|
||||||
consola.info('准备连接终端执行一次性指令:', host)
|
|
||||||
consola.log('连接信息', { username, port, authType })
|
|
||||||
let sshClient = new SSHClient()
|
|
||||||
execClient.push(sshClient)
|
|
||||||
sshClient
|
|
||||||
.on('ready', () => {
|
|
||||||
consola.success('连接终端成功:', host)
|
|
||||||
// socket.emit('connect_success', `已连接到终端:${ host }`)
|
|
||||||
execShell(socket, sshClient, curRes, resolve)
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
console.log(err)
|
|
||||||
consola.error('onekey终端连接失败:', err.level)
|
|
||||||
curRes.status = execStatusEnum.connectFail
|
|
||||||
curRes.result += err.message
|
|
||||||
socket.emit('output', execResult)
|
|
||||||
resolve(curRes)
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
...authInfo
|
|
||||||
// debug: (info) => console.log(info)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
consola.error('创建终端错误:', err.message)
|
|
||||||
curRes.status = execStatusEnum.connectFail
|
|
||||||
curRes.result += err.message
|
|
||||||
socket.emit('output', execResult)
|
|
||||||
resolve(curRes)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
await Promise.all(execPromise)
|
|
||||||
consola.success('onekey执行完成')
|
|
||||||
socket.emit('exec_complete')
|
|
||||||
sendNoticeAsync('onekey_complete', '批量指令执行完成', '请登录面板查看执行结果')
|
|
||||||
socket.disconnect()
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('onekey执行失败', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('disconnect', async (reason) => {
|
|
||||||
consola.info('onekey终端连接断开:', reason)
|
|
||||||
disconnectAllExecClient()
|
|
||||||
const { execSuccess, connectFail, execFail, execTimeout } = execStatusEnum
|
|
||||||
execResult.forEach(item => {
|
|
||||||
// 非服务端手动断开连接且命令执行状态为非完成\失败\超时, 判定为客户端主动中断
|
|
||||||
if (reason !== 'server namespace disconnect' && ![execSuccess, execFail, execTimeout, connectFail].includes(item.status)) {
|
|
||||||
item.status = execStatusEnum.socketInterrupt
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await onekeyDB.insertAsync(execResult)
|
|
||||||
isExecuting = false
|
|
||||||
execResult = []
|
|
||||||
execClient = []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
U2FsdGVkX1+P63MsIfF2N9/XM16sWy0/pZMWy+0Ptf+FhySv/AiawI4Pcf/HQU7Auxde+GszGb7+t+i1Ckngo6VK9PkwALR87GbqCJtGeMazZTkEGkmNuePdpej0O3oAuwITI1FOKPW4Xe4RIFAkJfghqCgUD0Ps0Y6sPwIxOX7fTi4TopNksRMQ5X+UvezrGnPsF5EC2CAPmKwtRFWVqx5csAFhifvMxwEA+WCA7l9KLLvcybGtY4RZf0uSLb6qGxrJBN/zbQ1MMxVw9JbUML09uQ3VKLvQMJZmjctIpZDr4YEMMMdDD/qDk64feV8Tc5VPENsyl2i9kxZ4Z3s5pUu6oDm+/GE2ag0OMITgg7Wc6QpeqlWJwgAeGuqxz2nnuQfGbhHv4g800Hwc7C6ylYgHHeSY+gx39PyDQy1tE7vtW/83ZQUSgWRXxMYTMHUgYKh6P3XG+HxJz6vRjpZqwjPIc3jd253EQnHVG4YZ7VxjBFwpcmidnkvKMa+dvQSZypOL3XLKlMSGdMWFbtVuw4MMYnTBadWkS3eekdVFtvpG5NRqga0TBxPeoISLsn717u9BYcROHvPzvX/MKG7S9CGClZb4mYbOPxKmENPT6AvQbCrzOlK6X/kHTLOxivc2O5uzL4CRXBKFDeaAUJqs/PLZuCfvdmMBKPiMQpCLtFBYCXoxDarggu2D8EZYcoxFPJ/YE7sy/bFa52hQ0V1wYXkhwez1Q4Q3PH1dqdzOESOI30KEOk9TwkEhdV69ymZ6rb6cXWy1KOph2Vo8dQhgNItgvNCCyEuojR54eXnh8x7R1FHmITrobfRZZOYmpFFRZntirTpkEDvt8sPR7G+STcfKc+OgNVoCSV2Ca79Ex0BVACRxWTrafC+VX80mQGldt3wwTk2P+7mcTl0NqH6F7hfzaDjHLy/pbd/78lJLToMz7K1/PaPfOAlVsD5MTh2hefAmqTDZRKfBHyQQjwgh2hNqpmzP2tcQqO5qeigk3fzvZjyXAX2Zqu85hc6QI0Quc4zRS1hb0uY93mnOLnOksTgCNPZHDCTmzT7N0v4D/oABq2VyFFDIvMlzy1kD1WnpmhPldfm+J268QnGfMoR7ob6quZB660xc3V/9zN16ZeVKAhFfzwVHApubxTOYtIIQWLzGj9Q2eqYCXf2p3n8CYF8YV3L1LT6FxEK//MOBXZZbf7JC7tkSWb0EdRf2wuxlDocrxXzSTuIFlpBuG4Dcjf9wVCn3NuQJhqVAdxSX98K/sJpKYPELyYwTvSwG8v2MApAAgv7v+NfgSswXGCVlYpeildrVM4AafoZjBVb4OJK5YvDgmhyhzKP8copwAwi2/tGK8x/fiT0oTQhR60c7wZBuaj3/D0uDxNCcRct5/DYvb3dU1qMDmPfw0BNE7xhHouVLM12tdSnZxxFWgq5747aOpD+yYfgQBxToXsAbfEZLZLfRXPqcNjJWdnY1ZlqARyrzFKbBlGu/HhdEpldDxhdOFLkjytrA7NzvKJKfWvxbtv+jkVYL6SNdLfRoaUe0ecPJH/Gq12FhRXXudPRQT/xqQ5TXCJZ41gkmd7B9oMuVrmpDhaSKkX1loSj/Mhezzq5vIrZPd3+qK7wMeFrxvY7E4wIlK1gZdyQXBzUh8VPQzY6KFEDV4fekqkxkdKm4oX8Ij3IJfuX52+lIJdhL3t3p/j7mLLc1L9hvRudgWnujLEOi8syJv8EkKARC9OmO0S1okf1R805fenbS6MiM7zoFqza6iNn/9uJwgzCjxoTqpLoPwU2OYQyN3h4LnRoV5qsCo/RUunrdkAAQp85iTMSTWm0ux9R/jy+IHe62zwfrCq9xsdqMZ/0MZPRAyGIh5Cr0JbhXwT5s8meI5MgjJ107+hfSdo0ufSqGnceTfvfNVGUt8KO2PWFwgb7fyAiirnIL53lgKOo8dD4tDVZjfClGdIG7KJFty5M3rzyCyPl5ZeQJRD6YZVZG3HPFt4BDPDecPMK63zvrX9zSK3FV5GyBgZQQVMQbou+gpnGdSGw3NI0agN47hRYjFsbvGPrVjBKiPrfnBKkd2lyqKvd2cns/yYmgGXtehYasnPLU01dvRHicQ+TduvMR4NYMCytiDbiKqwBaYI+yt7cKNSf4cQQT9b3DD8pl9dQhzrnAY0tHxSLCVZzl7LCbYzaITQRLzBoIzCmPrFGLj6j2HuGc+gQ3Hd2/HisVAYxbW6Fu0SXQa9nnGW7k+3AJFRs5EvF++QffNW2rMYaBqr+c1jfwURzc7dGD8PNtDJkx3T6eSKx9l4iWR5/LDW+SXrk2OCzMdznsBNCJJbaKzUqs1HlIEuwQ/hHMjk4M+f4/SmbECE3cQXd+nseEoUnKbn/MiWi2lNyjacXuLTtrjDITK9jIZD0Ixfci5Eer2fuPJw7RXr1rb8bjlZWttpjN45NORLB8usjiiEdklPjQMUmTjXgALATorkWt6vc76zB4Fbk5jOwJ53+W1RqvUxt5S+WIhH55jOxYHqrEppguVMw1w4RIceaL/CYunwHxiD5w58hjJ4bR5fXjwARwM/EDGHgPvfV0dDxhBxbBtKrcemUYlBI40wwbOuDgfdnfcJNVCxaVX0Hto/VuqXvfP3eoQT2Vasz96y2/GbSS0rBGtNn9EzPwGZvBrumbl4ezQJp+HjSN65GLXjDNY+ZajGaOs+pK1O8ooAqODnfAhPd3LkMxQxlkqNy8l4KFUPA1lqd8ohK+4UThmfRCQDKPLTdFV/oTvEdBo0d/oN3xJM9ValH+0e3w86b+cmXnLzUxUzeIhmXWW8ctGtNmzV2fewh8ChPl75MCU5d88uzC03YbfZHOOtokTqkhLYhz4il7KSnA+EFEWB7GOvVqnkM/LAwDcJ/1qvqBah+WnDs9uQXTF+QXn/N1q/+83lu9JQWK0crV2mTJHk8efTHBn6oEN8P9pXf2BCJQzbukA/q50QvQU33p8P2VOqL/bHwsaolIhWrys5lyM4pwpMqxvnP5YIFjEhGhcCHLtqL5jvETh3X78HHiEhjmNzwtj3wa/NrgvzJhepqWWGGBK1DEYoMj21cFPpUeB/+2
|
|
@ -1,18 +1,29 @@
|
|||||||
const rawPath = require('path')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const SFTPClient = require('ssh2-sftp-client')
|
|
||||||
const CryptoJS = require('crypto-js')
|
|
||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
|
const SFTPClient = require('ssh2-sftp-client')
|
||||||
|
const rawPath = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
||||||
const { sftpCacheDir } = require('../config')
|
const { sftpCacheDir } = require('../config')
|
||||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
const CryptoJS = require('crypto-js')
|
||||||
const { AESDecryptAsync } = require('../utils/encrypt')
|
|
||||||
const { isAllowedIp } = require('../utils/tools')
|
|
||||||
const { HostListDB, CredentialsDB } = require('../utils/db-class')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
const credentialsDB = new CredentialsDB().getInstance()
|
|
||||||
|
|
||||||
// 读取切片
|
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) => {
|
const pipeStream = (path, writeStream) => {
|
||||||
|
// console.log('path', path)
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const readStream = fs.createReadStream(path)
|
const readStream = fs.createReadStream(path)
|
||||||
readStream.on('end', () => {
|
readStream.on('end', () => {
|
||||||
@ -24,12 +35,12 @@ const pipeStream = (path, writeStream) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listenInput(sftpClient, socket) {
|
function listenInput(sftpClient, socket) {
|
||||||
socket.on('open_dir', async (path, tips = true) => {
|
socket.on('open_dir', async (path) => {
|
||||||
const exists = await sftpClient.exists(path)
|
const exists = await sftpClient.exists(path)
|
||||||
if (!exists) return socket.emit('not_exists_dir', tips ? '目录不存在或当前不可访问' : '')
|
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
||||||
try {
|
try {
|
||||||
let dirLs = await sftpClient.list(path)
|
let dirLs = await sftpClient.list(path)
|
||||||
socket.emit('dir_ls', dirLs, path)
|
socket.emit('dir_ls', dirLs)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('open_dir Error', error.message)
|
consola.error('open_dir Error', error.message)
|
||||||
socket.emit('sftp_error', error.message)
|
socket.emit('sftp_error', error.message)
|
||||||
@ -37,8 +48,7 @@ function listenInput(sftpClient, socket) {
|
|||||||
})
|
})
|
||||||
socket.on('rm_dir', async (path) => {
|
socket.on('rm_dir', async (path) => {
|
||||||
const exists = await sftpClient.exists(path)
|
const exists = await sftpClient.exists(path)
|
||||||
if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
||||||
consola.info('rm_dir: ', path)
|
|
||||||
try {
|
try {
|
||||||
let res = await sftpClient.rmdir(path, true) // 递归删除
|
let res = await sftpClient.rmdir(path, true) // 递归删除
|
||||||
socket.emit('rm_success', res)
|
socket.emit('rm_success', res)
|
||||||
@ -49,7 +59,7 @@ function listenInput(sftpClient, socket) {
|
|||||||
})
|
})
|
||||||
socket.on('rm_file', async (path) => {
|
socket.on('rm_file', async (path) => {
|
||||||
const exists = await sftpClient.exists(path)
|
const exists = await sftpClient.exists(path)
|
||||||
if (!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
||||||
try {
|
try {
|
||||||
let res = await sftpClient.delete(path)
|
let res = await sftpClient.delete(path)
|
||||||
socket.emit('rm_success', res)
|
socket.emit('rm_success', res)
|
||||||
@ -60,22 +70,20 @@ function listenInput(sftpClient, socket) {
|
|||||||
})
|
})
|
||||||
// socket.on('down_dir', async (path) => {
|
// socket.on('down_dir', async (path) => {
|
||||||
// const exists = await sftpClient.exists(path)
|
// const exists = await sftpClient.exists(path)
|
||||||
// if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
// if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
// let res = await sftpClient.downloadDir(path, sftpCacheDir)
|
// let res = await sftpClient.downloadDir(path, sftpCacheDir)
|
||||||
// socket.emit('down_dir_success', res)
|
// socket.emit('down_dir_success', res)
|
||||||
// })
|
// })
|
||||||
|
|
||||||
// 下载
|
|
||||||
socket.on('down_file', async ({ path, name, size, target = 'down' }) => {
|
socket.on('down_file', async ({ path, name, size, target = 'down' }) => {
|
||||||
// target: down or preview
|
// target: down or preview
|
||||||
const exists = await sftpClient.exists(path)
|
const exists = await sftpClient.exists(path)
|
||||||
if (!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', '文件不存在或当前不可访问')
|
||||||
try {
|
try {
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
let timer = null
|
let timer = null
|
||||||
let res = await sftpClient.fastGet(path, localPath, {
|
let res = await sftpClient.fastGet(path, localPath, {
|
||||||
step: step => {
|
step: step => {
|
||||||
if (timer) return
|
if(timer) return
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2
|
const percent = Math.ceil((step / size) * 100) // 下载进度为服务器下载到服务端的进度,前端无需*2
|
||||||
console.log(`从服务器下载进度:${ percent }%`)
|
console.log(`从服务器下载进度:${ percent }%`)
|
||||||
@ -87,7 +95,7 @@ function listenInput(sftpClient, socket) {
|
|||||||
consola.success('sftp下载成功: ', res)
|
consola.success('sftp下载成功: ', res)
|
||||||
let buffer = fs.readFileSync(localPath)
|
let buffer = fs.readFileSync(localPath)
|
||||||
let data = { buffer, name }
|
let data = { buffer, name }
|
||||||
switch (target) {
|
switch(target) {
|
||||||
case 'down':
|
case 'down':
|
||||||
socket.emit('down_file_success', data)
|
socket.emit('down_file_success', data)
|
||||||
break
|
break
|
||||||
@ -101,12 +109,10 @@ function listenInput(sftpClient, socket) {
|
|||||||
socket.emit('sftp_error', error.message)
|
socket.emit('sftp_error', error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 上传
|
|
||||||
socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
|
socket.on('up_file', async ({ targetPath, fullPath, name, file }) => {
|
||||||
// console.log({ targetPath, fullPath, name, file })
|
console.log({ targetPath, fullPath, name, file })
|
||||||
const exists = await sftpClient.exists(targetPath)
|
const exists = await sftpClient.exists(targetPath)
|
||||||
if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
try {
|
try {
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
fs.writeFileSync(localPath, file)
|
fs.writeFileSync(localPath, file)
|
||||||
@ -119,60 +125,43 @@ function listenInput(sftpClient, socket) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 上传目录先在目标sftp服务器创建目录
|
|
||||||
socket.on('create_remote_dir', async ({ targetDirPath, foldersName }) => {
|
|
||||||
let baseFolderPath = rawPath.posix.join(targetDirPath, foldersName[0].split('/')[0])
|
|
||||||
let baseFolderPathExists = await sftpClient.exists(baseFolderPath)
|
|
||||||
if (baseFolderPathExists) return socket.emit('create_remote_dir_exists', `远程目录已存在: ${ baseFolderPath }`)
|
|
||||||
consola.info('准备创建远程服务器目录:', foldersName)
|
|
||||||
for (const folderName of foldersName) {
|
|
||||||
const fullPath = rawPath.posix.join(targetDirPath, folderName)
|
|
||||||
const exists = await sftpClient.exists(fullPath)
|
|
||||||
if (exists) continue
|
|
||||||
await sftpClient.mkdir(fullPath, true)
|
|
||||||
socket.emit('create_remote_dir_progress', fullPath)
|
|
||||||
consola.info('创建目录:', fullPath)
|
|
||||||
}
|
|
||||||
socket.emit('create_remote_dir_success')
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 分片上传 */
|
/** 分片上传 */
|
||||||
// 1. 创建本地缓存目录
|
// 1. 创建本地缓存文件夹
|
||||||
let md5List = []
|
let md5List = []
|
||||||
socket.on('create_cache_dir', async ({ targetDirPath, name }) => {
|
socket.on('create_cache_dir', async ({ targetPath, name }) => {
|
||||||
// console.log({ targetDirPath, name })
|
// console.log({ targetPath, name })
|
||||||
const exists = await sftpClient.exists(targetDirPath)
|
const exists = await sftpClient.exists(targetPath)
|
||||||
if (!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', '文件夹不存在或当前不可访问')
|
||||||
md5List = []
|
md5List = []
|
||||||
const localPath = rawPath.join(sftpCacheDir, name)
|
const localPath = rawPath.join(sftpCacheDir, name)
|
||||||
fs.emptyDirSync(localPath) // 不存在会创建,存在则清空
|
if(fs.existsSync(localPath)) clearDir(localPath) // 清空目录
|
||||||
|
fs.mkdirSync(localPath, { recursive: true })
|
||||||
|
console.log('================create_cache_success================')
|
||||||
socket.emit('create_cache_success')
|
socket.emit('create_cache_success')
|
||||||
})
|
})
|
||||||
// 2. 上传分片到面板服务
|
|
||||||
socket.on('up_file_slice', async ({ name, sliceFile, fileIndex }) => {
|
socket.on('up_file_slice', async ({ name, sliceFile, fileIndex }) => {
|
||||||
// console.log('up_file_slice:', fileIndex, name)
|
// console.log('up_file_slice:', fileIndex, name)
|
||||||
try {
|
try {
|
||||||
let md5 = `${ fileIndex }.${ CryptoJS.MD5(fileIndex+name).toString() }`
|
let md5 = `${ fileIndex }.${ CryptoJS.MD5(fileIndex+name).toString() }`
|
||||||
const md5LocalPath = rawPath.join(sftpCacheDir, name, md5)
|
const localPath = rawPath.join(sftpCacheDir, name, md5)
|
||||||
md5List.push(md5LocalPath)
|
md5List.push(localPath)
|
||||||
fs.writeFileSync(md5LocalPath, sliceFile)
|
fs.writeFileSync(localPath, sliceFile)
|
||||||
socket.emit('up_file_slice_success', md5)
|
socket.emit('up_file_slice_success', md5)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('up_file_slice Error', error.message)
|
consola.error('up_file_slice Error', error.message)
|
||||||
socket.emit('up_file_slice_fail', error.message)
|
socket.emit('up_file_slice_fail', error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 3. 合并分片上传到服务器
|
socket.on('up_file_slice_over', async ({ name, fullPath, range, size }) => {
|
||||||
socket.on('up_file_slice_over', async ({ name, targetFilePath, range, size }) => {
|
const resultDirPath = rawPath.join(sftpCacheDir, name)
|
||||||
const md5CacheDirPath = rawPath.join(sftpCacheDir, name)
|
|
||||||
const resultFilePath = rawPath.join(sftpCacheDir, name, name)
|
const resultFilePath = rawPath.join(sftpCacheDir, name, name)
|
||||||
fs.ensureDirSync(md5CacheDirPath)
|
|
||||||
try {
|
try {
|
||||||
console.log('md5List: ', md5List)
|
console.log('md5List: ', md5List)
|
||||||
const arr = md5List.map((chunkFilePath, index) => {
|
const arr = md5List.map((chunkFilePath, index) => {
|
||||||
return pipeStream(
|
return pipeStream(
|
||||||
chunkFilePath,
|
chunkFilePath,
|
||||||
fs.createWriteStream(resultFilePath, { // 指定位置创建可写流
|
// 指定位置创建可写流
|
||||||
|
fs.createWriteStream(resultFilePath, {
|
||||||
start: index * range,
|
start: index * range,
|
||||||
end: (index + 1) * range
|
end: (index + 1) * range
|
||||||
})
|
})
|
||||||
@ -181,9 +170,9 @@ function listenInput(sftpClient, socket) {
|
|||||||
md5List = []
|
md5List = []
|
||||||
await Promise.all(arr)
|
await Promise.all(arr)
|
||||||
let timer = null
|
let timer = null
|
||||||
let res = await sftpClient.fastPut(resultFilePath, targetFilePath, {
|
let res = await sftpClient.fastPut(resultFilePath, fullPath, {
|
||||||
step: step => {
|
step: step => {
|
||||||
if (timer) return
|
if(timer) return
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
const percent = Math.ceil((step / size) * 100)
|
const percent = Math.ceil((step / size) * 100)
|
||||||
console.log(`上传服务器进度:${ percent }%`)
|
console.log(`上传服务器进度:${ percent }%`)
|
||||||
@ -194,14 +183,11 @@ function listenInput(sftpClient, socket) {
|
|||||||
})
|
})
|
||||||
consola.success('sftp上传成功: ', res)
|
consola.success('sftp上传成功: ', res)
|
||||||
socket.emit('up_file_success', res)
|
socket.emit('up_file_success', res)
|
||||||
|
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('sftp上传失败: ', error.message)
|
consola.error('sftp上传失败: ', error.message)
|
||||||
socket.emit('up_file_fail', error.message)
|
socket.emit('up_file_fail', error.message)
|
||||||
} finally {
|
clearDir(resultDirPath, true) // 传服务器后移除文件夹及其文件
|
||||||
fs.remove(md5CacheDirPath)
|
|
||||||
.then(() => {
|
|
||||||
console.log('clean md5CacheDirPath:', md5CacheDirPath)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -215,48 +201,42 @@ module.exports = (httpServer) => {
|
|||||||
})
|
})
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
if (!isAllowedIp(requestIP)) {
|
|
||||||
socket.emit('ip_forbidden', 'IP地址不在白名单中')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let sftpClient = new SFTPClient()
|
let sftpClient = new SFTPClient()
|
||||||
consola.success('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
|
|
||||||
socket.on('create', async ({ hostId, token }) => {
|
socket.on('create', async ({ host: ip, token }) => {
|
||||||
const { code } = await verifyAuthSync(token, requestIP)
|
const { code } = await verifyAuthSync(token, clientIp)
|
||||||
consola.log('code:', code)
|
if(code !== 1) {
|
||||||
if (code !== 1) {
|
|
||||||
socket.emit('token_verify_fail')
|
socket.emit('token_verify_fail')
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
|
||||||
if (!targetHostInfo) throw new Error(`Host with ID ${ hostId } not found`)
|
const hostList = await readHostList()
|
||||||
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
let { authType, host, port, username } = targetHostInfo
|
let { authType, host, port, username } = targetHostInfo
|
||||||
if (!host) return socket.emit('create_fail', `查找id【${ hostId }】凭证信息失败`)
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
let authInfo = { host, port, username }
|
let authInfo = { host, port, username }
|
||||||
|
|
||||||
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
if (authType === 'credential') {
|
if (authType === 'credential') {
|
||||||
let credentialId = await AESDecryptAsync(targetHostInfo[authType])
|
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
||||||
const sshRecordList = await credentialsDB.findAsync({})
|
const sshRecordList = await readSSHRecord()
|
||||||
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
authInfo.authType = sshRecord.authType
|
authInfo.authType = sshRecord.authType
|
||||||
authInfo[authInfo.authType] = await AESDecryptAsync(sshRecord[authInfo.authType])
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
} else {
|
} else {
|
||||||
authInfo[authType] = await AESDecryptAsync(targetHostInfo[authType])
|
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
||||||
}
|
}
|
||||||
consola.info('准备连接Sftp面板:', host)
|
consola.info('准备连接Sftp面板:', host)
|
||||||
targetHostInfo[targetHostInfo.authType] = await AESDecryptAsync(targetHostInfo[targetHostInfo.authType])
|
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
|
|
||||||
consola.log('连接信息', { username, port, authType })
|
consola.log('连接信息', { username, port, authType })
|
||||||
sftpClient
|
sftpClient
|
||||||
.connect(authInfo)
|
.connect(authInfo)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
consola.success('连接Sftp成功:', host)
|
consola.success('连接Sftp成功:', host)
|
||||||
fs.ensureDirSync(sftpCacheDir)
|
|
||||||
return sftpClient.list('/')
|
return sftpClient.list('/')
|
||||||
})
|
})
|
||||||
.then((rootLs) => {
|
.then((rootLs) => {
|
||||||
@ -280,10 +260,8 @@ module.exports = (httpServer) => {
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
sftpClient = null
|
sftpClient = null
|
||||||
fs.emptyDir(sftpCacheDir)
|
const cacheDir = rawPath.join(sftpCacheDir)
|
||||||
.then(() => {
|
clearDir(cacheDir)
|
||||||
consola.success('clean sftpCacheDir: ', sftpCacheDir)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,105 +1,31 @@
|
|||||||
const path = require('path')
|
|
||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const { Client: SSHClient } = require('ssh2')
|
const { Client: SSHClient } = require('ssh2')
|
||||||
const { verifyAuthSync } = require('../utils/verify-auth')
|
const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync } = require('../utils')
|
||||||
const { sendNoticeAsync } = require('../utils/notify')
|
|
||||||
const { isAllowedIp, ping } = require('../utils/tools')
|
|
||||||
const { AESDecryptAsync } = require('../utils/encrypt')
|
|
||||||
const { HostListDB, CredentialsDB } = require('../utils/db-class')
|
|
||||||
const decryptAndExecuteAsync = require('../utils/decrypt-file')
|
|
||||||
const hostListDB = new HostListDB().getInstance()
|
|
||||||
const credentialsDB = new CredentialsDB().getInstance()
|
|
||||||
|
|
||||||
async function getConnectionOptions(hostId) {
|
function createTerminal(socket, sshClient) {
|
||||||
const hostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||||
if (!hostInfo) throw new Error(`Host with ID ${ hostId } not found`)
|
|
||||||
let { authType, host, port, username, name } = hostInfo
|
|
||||||
let authInfo = { host, port, username }
|
|
||||||
try {
|
|
||||||
if (authType === 'credential') {
|
|
||||||
let credentialId = await AESDecryptAsync(hostInfo[authType])
|
|
||||||
const sshRecord = await credentialsDB.findOneAsync({ _id: credentialId })
|
|
||||||
authInfo.authType = sshRecord.authType
|
|
||||||
authInfo[authInfo.authType] = await AESDecryptAsync(sshRecord[authInfo.authType])
|
|
||||||
} else {
|
|
||||||
authInfo[authType] = await AESDecryptAsync(hostInfo[authType])
|
|
||||||
}
|
|
||||||
return { authInfo, name }
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`解密认证信息失败: ${ err.message }`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createInteractiveShell(socket, targetSSHClient) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
targetSSHClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
|
||||||
resolve(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', () => {
|
||||||
consola.info('交互终端已关闭')
|
consola.info('关闭终端')
|
||||||
targetSSHClient.end()
|
sshClient.end()
|
||||||
})
|
})
|
||||||
socket.emit('connect_shell_success') // 已连接终端,web端可以执行指令了
|
// web端输入
|
||||||
|
socket.on('input', key => {
|
||||||
|
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
||||||
|
stream.write(key)
|
||||||
})
|
})
|
||||||
})
|
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
||||||
}
|
|
||||||
|
|
||||||
async function createTerminal(hostId, socket, targetSSHClient) {
|
// 监听按键重置终端大小
|
||||||
return new Promise(async (resolve) => {
|
socket.on('resize', ({ rows, cols }) => {
|
||||||
const targetHostInfo = await hostListDB.findOneAsync({ _id: hostId })
|
// consola.info('更改tty终端行&列: ', { rows, cols })
|
||||||
if (!targetHostInfo) return socket.emit('create_fail', `查找hostId【${ hostId }】凭证信息失败`)
|
stream.setWindow(rows, cols)
|
||||||
let { connectByJumpHosts = null } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
|
|
||||||
let { authType, host, port, username, name, jumpHosts } = targetHostInfo
|
|
||||||
try {
|
|
||||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
|
||||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
|
||||||
if (jumpHostResult) {
|
|
||||||
targetConnectionOptions.sock = jumpHostResult.sock
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit('terminal_print_info', `准备连接目标终端: ${ name } - ${ host }`)
|
|
||||||
socket.emit('terminal_print_info', `连接信息: ssh ${ username }@${ host } -p ${ port } -> ${ authType }`)
|
|
||||||
|
|
||||||
consola.info('准备连接目标终端:', host)
|
|
||||||
consola.log('连接信息', { username, port, authType })
|
|
||||||
let closeNoticeFlag = false // 避免重复发送通知
|
|
||||||
targetSSHClient
|
|
||||||
.on('ready', async () => {
|
|
||||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录成功`)
|
|
||||||
socket.emit('terminal_print_info', `终端连接成功: ${ name } - ${ host }`)
|
|
||||||
consola.success('终端连接成功:', host)
|
|
||||||
socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
|
|
||||||
let stream = await createInteractiveShell(socket, targetSSHClient)
|
|
||||||
resolve(stream)
|
|
||||||
})
|
})
|
||||||
.on('close', (err) => {
|
|
||||||
if (closeNoticeFlag) return closeNoticeFlag = false
|
|
||||||
const closeReason = err ? '发生错误导致连接断开' : '正常断开连接'
|
|
||||||
consola.info(`终端连接断开(${ closeReason }): ${ host }`)
|
|
||||||
socket.emit('connect_close', { reason: closeReason })
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
closeNoticeFlag = true
|
|
||||||
sendNoticeAsync('host_login', '终端登录', `别名: ${ name } \n IP:${ host } \n 端口:${ port } \n 状态: 登录失败`)
|
|
||||||
consola.error('连接终端失败:', host, err.message)
|
|
||||||
socket.emit('connect_terminal_fail', err.message)
|
|
||||||
})
|
|
||||||
.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
|
|
||||||
finish([targetConnectionOptions[authType]])
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
tryKeyboard: true,
|
|
||||||
...targetConnectionOptions
|
|
||||||
// debug: (info) => console.log(info)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
consola.error('创建终端失败: ', host, err.message)
|
|
||||||
socket.emit('create_terminal_fail', err.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,57 +33,68 @@ module.exports = (httpServer) => {
|
|||||||
const serverIo = new Server(httpServer, {
|
const serverIo = new Server(httpServer, {
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*'
|
origin: '*' // 'http://localhost:8080'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let connectionCount = 0
|
|
||||||
|
|
||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
connectionCount++
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
consola.success(`terminal websocket 已连接 - 当前连接数: ${ connectionCount }`)
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
let sshClient = new SSHClient()
|
||||||
if (!isAllowedIp(requestIP)) {
|
|
||||||
socket.emit('ip_forbidden', 'IP地址不在白名单中')
|
|
||||||
socket.disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
consola.success('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
let targetSSHClient = null
|
|
||||||
socket.on('create', async ({ hostId, token }) => {
|
socket.on('create', async ({ host: ip, token }) => {
|
||||||
const { code } = await verifyAuthSync(token, requestIP)
|
const { code } = await verifyAuthSync(token, clientIp)
|
||||||
if (code !== 1) {
|
if (code !== 1) {
|
||||||
socket.emit('token_verify_fail')
|
socket.emit('token_verify_fail')
|
||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetSSHClient = new SSHClient()
|
const hostList = await readHostList()
|
||||||
let stream = null
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
function listenerInput(key) {
|
let { authType, host, port, username } = targetHostInfo
|
||||||
if (targetSSHClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
stream && stream.write(key)
|
let authInfo = { host, port, username }
|
||||||
}
|
// 统一使用commonKey解密
|
||||||
function resizeShell({ rows, cols }) {
|
|
||||||
stream && stream.setWindow(rows, cols)
|
|
||||||
}
|
|
||||||
socket.on('input', listenerInput)
|
|
||||||
socket.on('resize', resizeShell)
|
|
||||||
stream = await createTerminal(hostId, socket, targetSSHClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('get_ping', async (ip) => {
|
|
||||||
try {
|
try {
|
||||||
socket.emit('ping_data', await ping(ip, 2500))
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
} catch (error) {
|
if (authType === 'credential') {
|
||||||
socket.emit('ping_data', { success: false, msg: error.message })
|
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
||||||
|
const sshRecordList = await readSSHRecord()
|
||||||
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
|
authInfo.authType = sshRecord.authType
|
||||||
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
|
} else {
|
||||||
|
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
||||||
|
}
|
||||||
|
consola.info('准备连接终端:', host)
|
||||||
|
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
|
consola.log('连接信息', { username, port, authType })
|
||||||
|
sshClient
|
||||||
|
.on('ready', () => {
|
||||||
|
consola.success('连接终端成功:', host)
|
||||||
|
socket.emit('connect_success', `已连接到终端:${ host }`)
|
||||||
|
createTerminal(socket, sshClient)
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
console.log(err)
|
||||||
|
consola.error('连接终端失败:', err.level)
|
||||||
|
socket.emit('connect_fail', err.message)
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
...authInfo
|
||||||
|
// debug: (info) => console.log(info)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
consola.error('创建终端失败:', err.message)
|
||||||
|
socket.emit('create_fail', err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
connectionCount--
|
consola.info('终端连接断开:', reason)
|
||||||
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
|
sshClient.end()
|
||||||
|
sshClient.destroy()
|
||||||
|
sshClient = null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getConnectionOptions = getConnectionOptions
|
|
||||||
|
@ -1,40 +1,26 @@
|
|||||||
module.exports = (content) => {
|
module.exports = (content) => {
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html
|
||||||
<html>
|
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
<head>
|
|
||||||
<style>
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
<head>
|
||||||
margin: 15px 5px;
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
color: #333;
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
background-color: #f4f4f4;
|
</head>
|
||||||
line-height: 1.6;
|
|
||||||
}
|
<body style="margin: 0; padding: 0;text-align: center;">
|
||||||
.container {
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
background-color: #fff;
|
<tr>
|
||||||
padding: 20px;
|
<td>
|
||||||
border-radius: 8px;
|
<h3 style="font-size: 18px;color: #5992D3;padding:0 0 0 10px;">
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
${ content }
|
||||||
}
|
</h3>
|
||||||
h1 {
|
</td>
|
||||||
color: #4CAF50;
|
</tr>
|
||||||
}
|
</table>
|
||||||
p {
|
</body>
|
||||||
margin: 12px 0;
|
|
||||||
}
|
</html>
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<p>${ content }</p>
|
|
||||||
<p class="footer">通知发送时间: ${ new Date() }</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
`
|
||||||
}
|
}
|
@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>403 禁止访问</title>
|
|
||||||
<link rel="icon" href="data:;base64,=">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>403 禁止访问</h1>
|
|
||||||
<p>抱歉,您没有权限访问此页面。</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,22 +1,10 @@
|
|||||||
const Datastore = require('@seald-io/nedb')
|
const Datastore = require('@seald-io/nedb')
|
||||||
const {
|
const { credentialsDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath, scriptsDBPath } = require('../config')
|
||||||
credentialsDBPath,
|
|
||||||
hostListDBPath,
|
|
||||||
keyDBPath,
|
|
||||||
notifyDBPath,
|
|
||||||
notifyConfigDBPath,
|
|
||||||
groupConfDBPath,
|
|
||||||
scriptsDBPath,
|
|
||||||
onekeyDBPath,
|
|
||||||
logDBPath,
|
|
||||||
plusDBPath
|
|
||||||
} = require('../config')
|
|
||||||
|
|
||||||
module.exports.KeyDB = class KeyDB {
|
module.exports.KeyDB = class KeyDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!KeyDB.instance) {
|
if (!KeyDB.instance) {
|
||||||
KeyDB.instance = new Datastore({ filename: keyDBPath, autoload: true })
|
KeyDB.instance = new Datastore({ filename: keyDBPath, autoload: true })
|
||||||
// KeyDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
@ -28,7 +16,6 @@ module.exports.HostListDB = class HostListDB {
|
|||||||
constructor() {
|
constructor() {
|
||||||
if (!HostListDB.instance) {
|
if (!HostListDB.instance) {
|
||||||
HostListDB.instance = new Datastore({ filename: hostListDBPath, autoload: true })
|
HostListDB.instance = new Datastore({ filename: hostListDBPath, autoload: true })
|
||||||
// HostListDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
@ -36,23 +23,21 @@ module.exports.HostListDB = class HostListDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.CredentialsDB = class CredentialsDB {
|
module.exports.SshRecordDB = class SshRecordDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!CredentialsDB.instance) {
|
if (!SshRecordDB.instance) {
|
||||||
CredentialsDB.instance = new Datastore({ filename: credentialsDBPath, autoload: true })
|
SshRecordDB.instance = new Datastore({ filename: credentialsDBPath, autoload: true })
|
||||||
// CredentialsDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
return CredentialsDB.instance
|
return SshRecordDB.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.NotifyDB = class NotifyDB {
|
module.exports.NotifyDB = class NotifyDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!NotifyDB.instance) {
|
if (!NotifyDB.instance) {
|
||||||
NotifyDB.instance = new Datastore({ filename: notifyDBPath, autoload: true })
|
NotifyDB.instance = new Datastore({ filename: notifyConfDBPath, autoload: true })
|
||||||
// NotifyDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
@ -60,23 +45,10 @@ module.exports.NotifyDB = class NotifyDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.NotifyConfigDB = class NotifyConfigDB {
|
|
||||||
constructor() {
|
|
||||||
if (!NotifyConfigDB.instance) {
|
|
||||||
NotifyConfigDB.instance = new Datastore({ filename: notifyConfigDBPath, autoload: true })
|
|
||||||
// NotifyConfigDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getInstance() {
|
|
||||||
return NotifyConfigDB.instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.GroupDB = class GroupDB {
|
module.exports.GroupDB = class GroupDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!GroupDB.instance) {
|
if (!GroupDB.instance) {
|
||||||
GroupDB.instance = new Datastore({ filename: groupConfDBPath, autoload: true })
|
GroupDB.instance = new Datastore({ filename: groupConfDBPath, autoload: true })
|
||||||
// GroupDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
@ -84,49 +56,24 @@ module.exports.GroupDB = class GroupDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.EmailNotifyDB = class EmailNotifyDB {
|
||||||
|
constructor() {
|
||||||
|
if (!EmailNotifyDB.instance) {
|
||||||
|
EmailNotifyDB.instance = new Datastore({ filename: emailNotifyDBPath, autoload: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getInstance() {
|
||||||
|
return EmailNotifyDB.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.ScriptsDB = class ScriptsDB {
|
module.exports.ScriptsDB = class ScriptsDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!ScriptsDB.instance) {
|
if (!ScriptsDB.instance) {
|
||||||
ScriptsDB.instance = new Datastore({ filename: scriptsDBPath, autoload: true })
|
ScriptsDB.instance = new Datastore({ filename: scriptsDBPath, autoload: true })
|
||||||
// ScriptsDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInstance() {
|
getInstance() {
|
||||||
return ScriptsDB.instance
|
return ScriptsDB.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.OnekeyDB = class OnekeyDB {
|
|
||||||
constructor() {
|
|
||||||
if (!OnekeyDB.instance) {
|
|
||||||
OnekeyDB.instance = new Datastore({ filename: onekeyDBPath, autoload: true })
|
|
||||||
// OnekeyDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getInstance() {
|
|
||||||
return OnekeyDB.instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.LogDB = class LogDB {
|
|
||||||
constructor() {
|
|
||||||
if (!LogDB.instance) {
|
|
||||||
LogDB.instance = new Datastore({ filename: logDBPath, autoload: true })
|
|
||||||
// LogDB.instance.setAutocompactionInterval(5000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getInstance() {
|
|
||||||
return LogDB.instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.PlusDB = class PlusDB {
|
|
||||||
constructor() {
|
|
||||||
if (!PlusDB.instance) {
|
|
||||||
PlusDB.instance = new Datastore({ filename: plusDBPath, autoload: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getInstance() {
|
|
||||||
return PlusDB.instance
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const CryptoJS = require('crypto-js')
|
|
||||||
const { AESDecryptAsync } = require('./encrypt')
|
|
||||||
const { PlusDB } = require('./db-class')
|
|
||||||
const plusDB = new PlusDB().getInstance()
|
|
||||||
|
|
||||||
function decryptAndExecuteAsync(plusPath) {
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
try {
|
|
||||||
let { decryptKey } = await plusDB.findOneAsync({})
|
|
||||||
if (!decryptKey) {
|
|
||||||
throw new Error('缺少解密密钥')
|
|
||||||
}
|
|
||||||
decryptKey = await AESDecryptAsync(decryptKey)
|
|
||||||
const encryptedContent = fs.readFileSync(plusPath, 'utf-8')
|
|
||||||
const bytes = CryptoJS.AES.decrypt(encryptedContent, decryptKey)
|
|
||||||
const decryptedContent = bytes.toString(CryptoJS.enc.Utf8)
|
|
||||||
if (!decryptedContent) {
|
|
||||||
throw new Error('解密失败,请检查密钥是否正确')
|
|
||||||
}
|
|
||||||
const customRequire = (modulePath) => {
|
|
||||||
if (modulePath.startsWith('.')) {
|
|
||||||
const absolutePath = path.resolve(path.dirname(plusPath), modulePath)
|
|
||||||
return require(absolutePath)
|
|
||||||
}
|
|
||||||
return require(modulePath)
|
|
||||||
}
|
|
||||||
const module = {
|
|
||||||
exports: {},
|
|
||||||
require: customRequire,
|
|
||||||
__filename: plusPath,
|
|
||||||
__dirname: path.dirname(plusPath)
|
|
||||||
}
|
|
||||||
const wrapper = Function('module', 'exports', 'require', '__filename', '__dirname',
|
|
||||||
decryptedContent + '\n return module.exports;'
|
|
||||||
)
|
|
||||||
const exports = wrapper(
|
|
||||||
module,
|
|
||||||
module.exports,
|
|
||||||
customRequire,
|
|
||||||
module.__filename,
|
|
||||||
module.__dirname
|
|
||||||
)
|
|
||||||
resolve(exports)
|
|
||||||
} catch (error) {
|
|
||||||
consola.info('解锁plus功能失败: ', error.message)
|
|
||||||
resolve(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = decryptAndExecuteAsync
|
|
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 = (await readUserEmailList()).find(({ auth }) => auth.user === toEmail)
|
||||||
|
if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`)
|
||||||
|
let { target } = userEmail
|
||||||
|
let emailServerConf = (await readSupportEmailList()).find((item) => item.target === target)
|
||||||
|
if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`)
|
||||||
|
const timeout = 1000*5
|
||||||
|
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 = await 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
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const rawCrypto = require('crypto')
|
const rawCrypto = require('crypto')
|
||||||
const NodeRSA = require('node-rsa')
|
const NodeRSA = require('node-rsa')
|
||||||
const { KeyDB } = require('./db-class')
|
const { readKey } = require('./storage.js')
|
||||||
const keyDB = new KeyDB().getInstance()
|
|
||||||
|
|
||||||
// rsa非对称 私钥解密
|
// rsa非对称 私钥解密
|
||||||
const RSADecryptAsync = async (ciphertext) => {
|
const RSADecryptSync = async (ciphertext) => {
|
||||||
if (!ciphertext) return
|
if (!ciphertext) return
|
||||||
let { privateKey } = await keyDB.findOneAsync({})
|
let { privateKey } = await readKey()
|
||||||
privateKey = await AESDecryptAsync(privateKey) // 先解密私钥
|
privateKey = await AESDecryptSync(privateKey) // 先解密私钥
|
||||||
const rsakey = new NodeRSA(privateKey)
|
const rsakey = new NodeRSA(privateKey)
|
||||||
rsakey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' }) // Must Set It When Frontend Use jsencrypt
|
rsakey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' }) // Must Set It When Frontend Use jsencrypt
|
||||||
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
||||||
@ -16,17 +15,17 @@ const RSADecryptAsync = async (ciphertext) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// aes对称 加密(default commonKey)
|
// aes对称 加密(default commonKey)
|
||||||
const AESEncryptAsync = async (text, key) => {
|
const AESEncryptSync = async (text, key) => {
|
||||||
if (!text) return
|
if(!text) return
|
||||||
let { commonKey } = await keyDB.findOneAsync({})
|
let { commonKey } = await readKey()
|
||||||
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
||||||
return ciphertext
|
return ciphertext
|
||||||
}
|
}
|
||||||
|
|
||||||
// aes对称 解密(default commonKey)
|
// aes对称 解密(default commonKey)
|
||||||
const AESDecryptAsync = async (ciphertext, key) => {
|
const AESDecryptSync = async (ciphertext, key) => {
|
||||||
if (!ciphertext) return
|
if(!ciphertext) return
|
||||||
let { commonKey } = await keyDB.findOneAsync({})
|
let { commonKey } = await readKey()
|
||||||
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
||||||
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
||||||
return originalText
|
return originalText
|
||||||
@ -38,8 +37,8 @@ const SHA1Encrypt = (clearText) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
RSADecryptAsync,
|
RSADecryptSync,
|
||||||
AESEncryptAsync,
|
AESEncryptSync,
|
||||||
AESDecryptAsync,
|
AESDecryptSync,
|
||||||
SHA1Encrypt
|
SHA1Encrypt
|
||||||
}
|
}
|
@ -1,93 +0,0 @@
|
|||||||
const { getLocalNetIP } = require('./tools')
|
|
||||||
const { AESEncryptAsync } = require('./encrypt')
|
|
||||||
const version = require('../../package.json').version
|
|
||||||
const { plusServer1, plusServer2 } = require('./plus-server')
|
|
||||||
const { PlusDB } = require('./db-class')
|
|
||||||
const plusDB = new PlusDB().getInstance()
|
|
||||||
|
|
||||||
async function getLicenseInfo(key = '') {
|
|
||||||
const { key: plusKey } = await plusDB.findOneAsync({}) || {}
|
|
||||||
// console.log('plusKey: ', plusKey)
|
|
||||||
// console.log('key: ', key)
|
|
||||||
// console.log('process.env.PLUS_KEY: ', process.env.PLUS_KEY)
|
|
||||||
key = key || plusKey || process.env.PLUS_KEY
|
|
||||||
if (!key || key.length < 16) return { success: false, msg: 'Invalid Plus Key' }
|
|
||||||
let ip = ''
|
|
||||||
if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) {
|
|
||||||
ip = global.serverIp
|
|
||||||
consola.log('get server ip by cache: ', ip)
|
|
||||||
} else {
|
|
||||||
ip = await getLocalNetIP()
|
|
||||||
global.serverIp = ip
|
|
||||||
global.getServerIpLastTime = Date.now()
|
|
||||||
consola.log('get server ip by net: ', ip)
|
|
||||||
}
|
|
||||||
if (!ip) {
|
|
||||||
consola.error('activate plus failed: get public ip failed')
|
|
||||||
global.serverIp = ''
|
|
||||||
return { success: false, msg: 'get public ip failed' }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let response
|
|
||||||
let method = 'POST'
|
|
||||||
let body = JSON.stringify({ ip, key, version })
|
|
||||||
let headers = { 'Content-Type': 'application/json' }
|
|
||||||
let timeout = 10000
|
|
||||||
try {
|
|
||||||
response = await fetch(plusServer1 + '/api/licenses/activate', {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
timeout
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok && (response.status !== 403)) {
|
|
||||||
throw new Error('port1 error')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
consola.log('retry to activate plus by backup server')
|
|
||||||
response = await fetch(plusServer2 + '/api/licenses/activate', {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
timeout
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
consola.log('activate plus failed: ', response.status)
|
|
||||||
if (response.status === 403) {
|
|
||||||
const errMsg = await response.json()
|
|
||||||
throw { errMsg, clear: true }
|
|
||||||
}
|
|
||||||
throw Error({ errMsg: `HTTP error! status: ${ response.status }` })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { success, data } = await response.json()
|
|
||||||
if (success) {
|
|
||||||
let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data
|
|
||||||
decryptKey = await AESEncryptAsync(decryptKey)
|
|
||||||
consola.success('activate plus success')
|
|
||||||
const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs }
|
|
||||||
let count = await plusDB.countAsync({})
|
|
||||||
if (count === 0) {
|
|
||||||
await plusDB.insertAsync(plusData)
|
|
||||||
} else {
|
|
||||||
await plusDB.removeAsync({}, { multi: true })
|
|
||||||
await plusDB.insertAsync(plusData)
|
|
||||||
}
|
|
||||||
return { success: true, msg: '激活成功' }
|
|
||||||
}
|
|
||||||
consola.error('activate plus failed: ', data)
|
|
||||||
return { success: false, msg: '激活失败' }
|
|
||||||
} catch (error) {
|
|
||||||
consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`)
|
|
||||||
if (error.clear) {
|
|
||||||
await plusDB.removeAsync({}, { multi: true })
|
|
||||||
}
|
|
||||||
return { success: false, msg: error.message || error.errMsg?.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = getLicenseInfo
|
|
55
server/app/utils/index.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const {
|
||||||
|
readSSHRecord,
|
||||||
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList,
|
||||||
|
readScriptList,
|
||||||
|
writeScriptList
|
||||||
|
} = require('./storage')
|
||||||
|
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
|
||||||
|
const { verifyAuthSync, isProd } = require('./verify-auth')
|
||||||
|
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
|
||||||
|
const { emailTransporter, sendEmailToConfList } = require('./email')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getNetIPInfo,
|
||||||
|
throwError,
|
||||||
|
isIP,
|
||||||
|
randomStr,
|
||||||
|
getUTCDate,
|
||||||
|
formatTimestamp,
|
||||||
|
verifyAuthSync,
|
||||||
|
isProd,
|
||||||
|
RSADecryptSync,
|
||||||
|
AESEncryptSync,
|
||||||
|
AESDecryptSync,
|
||||||
|
SHA1Encrypt,
|
||||||
|
readSSHRecord,
|
||||||
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
emailTransporter,
|
||||||
|
sendEmailToConfList,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList,
|
||||||
|
readScriptList,
|
||||||
|
writeScriptList
|
||||||
|
}
|
@ -1,101 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
const decryptAndExecuteAsync = require('./decrypt-file')
|
|
||||||
const nodemailer = require('nodemailer')
|
|
||||||
const axios = require('axios')
|
|
||||||
const commonTemp = require('../template/commonTemp')
|
|
||||||
const { NotifyDB, NotifyConfigDB } = require('./db-class')
|
|
||||||
const notifyConfigDB = new NotifyConfigDB().getInstance()
|
|
||||||
const notifyDB = new NotifyDB().getInstance()
|
|
||||||
|
|
||||||
function sendServerChan(sendKey, title, content) {
|
|
||||||
return new Promise((async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
consola.info('server酱通知预发送: ', title)
|
|
||||||
const url = `https://sctapi.ftqq.com/${ sendKey }.send`
|
|
||||||
const params = new URLSearchParams({ text: title, desp: content })
|
|
||||||
let { data } = await axios.post(url, params, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
resolve(data)
|
|
||||||
consola.info('server酱通知发送成功: ', title)
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
consola.error('server酱通知发送失败: ', error)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendEmail({ service, user, pass }, title, content) {
|
|
||||||
return new Promise((async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
consola.info('邮箱通知预发送: ', title)
|
|
||||||
let transporter = nodemailer.createTransport({
|
|
||||||
service,
|
|
||||||
auth: {
|
|
||||||
user,
|
|
||||||
pass
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: user,
|
|
||||||
to: user,
|
|
||||||
subject: title,
|
|
||||||
// text: '', // 纯文本版本内容,如果收件人的邮件客户端不支持HTML显示,就会显示这个文本
|
|
||||||
html: commonTemp(content)
|
|
||||||
})
|
|
||||||
consola.info('邮件通知发送成功: ', title)
|
|
||||||
resolve()
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
consola.error('邮件通知发送失败: ', error)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 异步发送通知
|
|
||||||
async function sendNoticeAsync(noticeAction, title, content) {
|
|
||||||
try {
|
|
||||||
let notifyList = await notifyDB.findAsync({})
|
|
||||||
let { sw } = notifyList.find((item) => item.type === noticeAction) // 获取对应动作的通知开关
|
|
||||||
// console.log('notify swtich: ', noticeAction, sw)
|
|
||||||
if (!sw) return consola.info('通知开关关闭, 不发送通知: ', noticeAction)
|
|
||||||
let notifyConfig = await notifyConfigDB.findOneAsync({})
|
|
||||||
let { type } = notifyConfig
|
|
||||||
if (!type) return consola.error('通知类型不存在: ', type)
|
|
||||||
title = `EasyNode-${ title }`
|
|
||||||
content += `\n通知发送时间:${ new Date() }`
|
|
||||||
switch (type) {
|
|
||||||
case 'sct':
|
|
||||||
let { sendKey } = notifyConfig['sct']
|
|
||||||
if (!sendKey) return consola.info('未发送server酱通知, sendKey 为空')
|
|
||||||
await sendServerChan(sendKey, title, content)
|
|
||||||
break
|
|
||||||
case 'email':
|
|
||||||
let { service, user, pass } = notifyConfig['email']
|
|
||||||
if (!service || !user || !pass) return consola.info('未发送邮件通知通知, 未配置邮箱: ', { service, user, pass })
|
|
||||||
await sendEmail({ service, user, pass }, title, content)
|
|
||||||
break
|
|
||||||
case 'tg':
|
|
||||||
let { token, chatId } = notifyConfig['tg']
|
|
||||||
if (!token || !chatId) return consola.info('未发送Telegram通知, 未配置token或chatId: ', { token, chatId })
|
|
||||||
let { sendTg } = await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))
|
|
||||||
if (!sendTg) return consola.info('未发送Telegram通知, Plus功能解析失败')
|
|
||||||
await sendTg({ token, chatId }, title, content)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
consola.info('未配置通知类型: ', type)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
consola.error('通知发送失败: ', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sendNoticeAsync,
|
|
||||||
sendServerChan,
|
|
||||||
sendEmail
|
|
||||||
}
|
|
84
server/app/utils/os-data.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
const osu = require('node-os-utils')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
let cpu = osu.cpu
|
||||||
|
let mem = osu.mem
|
||||||
|
let drive = osu.drive
|
||||||
|
let netstat = osu.netstat
|
||||||
|
let osuOs = osu.os
|
||||||
|
let users = osu.users
|
||||||
|
|
||||||
|
async function cpuInfo() {
|
||||||
|
let cpuUsage = await cpu.usage(200)
|
||||||
|
let cpuCount = cpu.count()
|
||||||
|
let cpuModel = cpu.model()
|
||||||
|
return {
|
||||||
|
cpuUsage,
|
||||||
|
cpuCount,
|
||||||
|
cpuModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function memInfo() {
|
||||||
|
let memInfo = await mem.info()
|
||||||
|
return {
|
||||||
|
...memInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function driveInfo() {
|
||||||
|
let driveInfo = {}
|
||||||
|
try {
|
||||||
|
driveInfo = await drive.info()
|
||||||
|
} catch {
|
||||||
|
// console.log(driveInfo)
|
||||||
|
}
|
||||||
|
return driveInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async function netstatInfo() {
|
||||||
|
let netstatInfo = await netstat.inOut()
|
||||||
|
return netstatInfo === 'not supported' ? {} : netstatInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async function osInfo() {
|
||||||
|
let type = os.type()
|
||||||
|
let platform = os.platform()
|
||||||
|
let release = os.release()
|
||||||
|
let uptime = osuOs.uptime()
|
||||||
|
let ip = osuOs.ip()
|
||||||
|
let hostname = osuOs.hostname()
|
||||||
|
let arch = osuOs.arch()
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
platform,
|
||||||
|
release,
|
||||||
|
ip,
|
||||||
|
hostname,
|
||||||
|
arch,
|
||||||
|
uptime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openedCount() {
|
||||||
|
let openedCount = await users.openedCount()
|
||||||
|
return openedCount === 'not supported' ? 0 : openedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
let data = {}
|
||||||
|
try {
|
||||||
|
data = {
|
||||||
|
cpuInfo: await cpuInfo(),
|
||||||
|
memInfo: await memInfo(),
|
||||||
|
driveInfo: await driveInfo(),
|
||||||
|
netstatInfo: await netstatInfo(),
|
||||||
|
osInfo: await osInfo(),
|
||||||
|
openedCount: await openedCount()
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
} catch(err){
|
||||||
|
console.error('获取系统信息出错:', err)
|
||||||
|
return err.toString()
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plusServer1: 'https://en1.221022.xyz',
|
|
||||||
plusServer2: 'https://en2.221022.xyz'
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
U2FsdGVkX1+Gus2FIC0WsNp0rUXPA+Ui1NQUjtnqP6Ycb1pyHglCADvKu51oxYaGJ0ZdoRZYo7YP3tQgIhp3f96WxP1/QFdypVrVlS7+jbAH6Gzc4CPlD3UeFsCm1j32ArFX60tPSSkq6+DJ3OF6pIVxstGIbCkmv5NQaf0J95zCxgqGm+fo/nZmZ6oj21uspGWZjhHssFRol0KpzINFDSWE9+/hJ43ybT5G6OHvEiaF83YH6h3CXAa6zz2zV18LKvnO8A4nTYR2/EBmGiP6NE3YqQ7hTE7SFmEDtRaxKJfyBxs0bHDCcFifVZh8GE25VyDwvOihUHztgvIRMh9vkgehzx9YN3sZdAsBJqcWyqi1mEPZU/l+zq2tbO+EczCvz6JQ77RZToQxm0vXzJc/ctcCEoVvjDx1pJhsQiTj5tJirFgcYz4VC7ihFYIq2XUQNISZaLynpYUUPdjvIfXGcvk0500SK9VAKb6603Z3fABdsENDGuxl2UKXMed4sL/PFwLy9siEX3BgMg1hFFiwoqqEp/x75341BoeRavEIJBEv8BdTS66mel1lUa/L3so7LyjGpdgfzOZlv+0t6Uhzy82HwYkAWmvuYpK6s6JItsG1ftYrOBzHZbpu36wn0e4N4NLqBnm6Hx1+tQJY7lTmgokgUy+5sVtp4LEsTbgE64HbDLYhME4m/3Yw5ij5D1OhoNwm/9r6MEYyJOyv8j8nDjudLRe1YQ0D2JLQsr04LYpVrjU1+Tsg780K0j0JdnFfVhe/SdkVU8nbkIIfRkv/86N6U2ZQaCYaScZmKYdBQmsK//I2yuYym0tM5q2d5kesYTxy8uAtVIXL1rE065eZFPlg/7Mgu0sqUsspG+EeDJE=
|
|
290
server/app/utils/storage.js
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB, ScriptsDB } = require('./db-class')
|
||||||
|
|
||||||
|
const readKey = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const keyDB = new KeyDB().getInstance()
|
||||||
|
keyDB.findOne({}, (err, doc) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(doc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeKey = async (keyObj = {}) => {
|
||||||
|
const keyDB = new KeyDB().getInstance()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
keyDB.update({}, { $set: keyObj }, { upsert: true }, (err, numReplaced) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
keyDB.compactDatafile()
|
||||||
|
resolve(numReplaced)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSSHRecord = async () => {
|
||||||
|
const sshRecordDB = new SshRecordDB().getInstance()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
sshRecordDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取ssh-record-db错误: ', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeSSHRecord = async (record = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sshRecordDB = new SshRecordDB().getInstance()
|
||||||
|
sshRecordDB.remove({}, { multi: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('清空SSHRecord出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
sshRecordDB.insert(record, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的ssh记录出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
sshRecordDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readHostList = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hostListDB = new HostListDB().getInstance()
|
||||||
|
hostListDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取host-list-db错误:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeHostList = async (record = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hostListDB = new HostListDB().getInstance()
|
||||||
|
hostListDB.remove({}, { multi: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('清空HostList出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
// 插入新的数据列表
|
||||||
|
hostListDB.insert(record, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的HostList出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
hostListDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readEmailNotifyConf = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||||
|
emailNotifyDB.findOne({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取email-notify-conf-db错误:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const writeUserEmailList = (user) => {
|
||||||
|
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let support = await readSupportEmailList()
|
||||||
|
const emailConf = { support, user }
|
||||||
|
emailNotifyDB.update({}, { $set: emailConf }, { upsert: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject({ code: -1, msg: err.message || err })
|
||||||
|
} else {
|
||||||
|
emailNotifyDB.compactDatafile()
|
||||||
|
resolve({ code: 0 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSupportEmailList = async () => {
|
||||||
|
let support = []
|
||||||
|
try {
|
||||||
|
support = (await readEmailNotifyConf()).support
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取email support错误: ', error)
|
||||||
|
}
|
||||||
|
return support
|
||||||
|
}
|
||||||
|
|
||||||
|
const readUserEmailList = async () => {
|
||||||
|
let user = []
|
||||||
|
try {
|
||||||
|
user = (await readEmailNotifyConf()).user
|
||||||
|
} catch (error) {
|
||||||
|
consola.error('读取email config错误: ', error)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNotifySwByType = async (type) => {
|
||||||
|
if (!type) throw Error('missing params: type')
|
||||||
|
try {
|
||||||
|
let notifyList = await readNotifyList()
|
||||||
|
let { sw } = notifyList.find((item) => item.type === type)
|
||||||
|
return sw
|
||||||
|
} catch (error) {
|
||||||
|
consola.error(`通知类型[${ type }]不存在`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readNotifyList = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const notifyDB = new NotifyDB().getInstance()
|
||||||
|
notifyDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取notify list错误: ', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeNotifyList = async (notifyList) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const notifyDB = new NotifyDB().getInstance()
|
||||||
|
notifyDB.remove({}, { multi: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('清空notify list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
notifyDB.insert(notifyList, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的notify list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
notifyDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readGroupList = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const groupDB = new GroupDB().getInstance()
|
||||||
|
groupDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取group list错误: ', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeGroupList = async (list = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const groupDB = new GroupDB().getInstance()
|
||||||
|
groupDB.remove({}, { multi: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('清空group list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
groupDB.insert(list, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的group list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
groupDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readScriptList = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptsDB = new ScriptsDB().getInstance()
|
||||||
|
scriptsDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取scripts list错误: ', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeScriptList = async (list = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptsDB = new ScriptsDB().getInstance()
|
||||||
|
scriptsDB.remove({}, { multi: true }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('清空scripts list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
scriptsDB.insert(list, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的group list出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
scriptsDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
readSSHRecord,
|
||||||
|
writeSSHRecord,
|
||||||
|
readHostList,
|
||||||
|
writeHostList,
|
||||||
|
readKey,
|
||||||
|
writeKey,
|
||||||
|
readNotifyList,
|
||||||
|
getNotifySwByType,
|
||||||
|
writeNotifyList,
|
||||||
|
readGroupList,
|
||||||
|
writeGroupList,
|
||||||
|
readSupportEmailList,
|
||||||
|
readUserEmailList,
|
||||||
|
writeUserEmailList,
|
||||||
|
readScriptList,
|
||||||
|
writeScriptList
|
||||||
|
}
|
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
|
@ -1,7 +1,4 @@
|
|||||||
const { exec } = require('child_process')
|
|
||||||
const os = require('os')
|
|
||||||
const net = require('net')
|
const net = require('net')
|
||||||
const iconv = require('iconv-lite')
|
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const request = axios.create({ timeout: 3000 })
|
const request = axios.create({ timeout: 3000 })
|
||||||
|
|
||||||
@ -39,37 +36,37 @@ const getNetIPInfo = async (searchIp = '') => {
|
|||||||
let [ipApi, ipwho, ipdata, ipinfo, ipgeolocation, ipApi01, ip138] = result
|
let [ipApi, ipwho, ipdata, ipinfo, ipgeolocation, ipApi01, ip138] = result
|
||||||
|
|
||||||
let searchResult = []
|
let searchResult = []
|
||||||
if (ipApi.status === 'fulfilled') {
|
if(ipApi.status === 'fulfilled') {
|
||||||
let { query: ip, country, regionName, city } = ipApi.value?.data || {}
|
let { query: ip, country, regionName, city } = ipApi.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipwho.status === 'fulfilled') {
|
if(ipwho.status === 'fulfilled') {
|
||||||
let { ip, country, region: regionName, city } = ipwho.value?.data || {}
|
let { ip, country, region: regionName, city } = ipwho.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipdata.status === 'fulfilled') {
|
if(ipdata.status === 'fulfilled') {
|
||||||
let { ip, country_name: country, region: regionName, city } = ipdata.value?.data || {}
|
let { ip, country_name: country, region: regionName, city } = ipdata.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipinfo.status === 'fulfilled') {
|
if(ipinfo.status === 'fulfilled') {
|
||||||
let { ip, country, region: regionName, city } = ipinfo.value?.data || {}
|
let { ip, country, region: regionName, city } = ipinfo.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipgeolocation.status === 'fulfilled') {
|
if(ipgeolocation.status === 'fulfilled') {
|
||||||
let { ip, country_name: country, state_prov: regionName, city } = ipgeolocation.value?.data || {}
|
let { ip, country_name: country, state_prov: regionName, city } = ipgeolocation.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipApi01.status === 'fulfilled') {
|
if(ipApi01.status === 'fulfilled') {
|
||||||
let { ip, country_name: country, region: regionName, city } = ipApi01.value?.data || {}
|
let { ip, country_name: country, region: regionName, city } = ipApi01.value?.data || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ip138.status === 'fulfilled') {
|
if(ip138.status === 'fulfilled') {
|
||||||
let [res] = ip138.value?.data?.data || []
|
let [res] = ip138.value?.data?.data || []
|
||||||
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
||||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||||
@ -89,35 +86,6 @@ const getNetIPInfo = async (searchIp = '') => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLocalNetIP = async () => {
|
|
||||||
try {
|
|
||||||
let ipUrls = [
|
|
||||||
'http://whois.pconline.com.cn/ipJson.jsp?json=true',
|
|
||||||
'https://www.ip.cn/api/index?ip=&type=0',
|
|
||||||
'https://freeipapi.com/api/json'
|
|
||||||
]
|
|
||||||
let result = await Promise.allSettled(ipUrls.map(url => axios.get(url)))
|
|
||||||
let [pconline, ipCN, freeipapi] = result
|
|
||||||
if (pconline.status === 'fulfilled') {
|
|
||||||
let ip = pconline.value?.data?.ip
|
|
||||||
if (ip) return ip
|
|
||||||
}
|
|
||||||
if (ipCN.status === 'fulfilled') {
|
|
||||||
let ip = ipCN.value?.data?.ip
|
|
||||||
consola.log('ipCN:', ip)
|
|
||||||
if (ip) return ip
|
|
||||||
}
|
|
||||||
if (freeipapi.status === 'fulfilled') {
|
|
||||||
let ip = pconline.value?.data?.ipAddress
|
|
||||||
if (ip) return ip
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getIpInfo Error: ', error?.message || error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocalIP(ip) {
|
function isLocalIP(ip) {
|
||||||
// Check if IPv4 or IPv6 address
|
// Check if IPv4 or IPv6 address
|
||||||
const isIPv4 = net.isIPv4(ip)
|
const isIPv4 = net.isIPv4(ip)
|
||||||
@ -188,7 +156,7 @@ const isIP = (ip = '') => {
|
|||||||
return isIPv4.test(ip) || isIPv6.test(ip)
|
return isIPv4.test(ip) || isIPv6.test(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomStr = (len) => {
|
const randomStr = (len) =>{
|
||||||
len = len || 16
|
len = len || 16
|
||||||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
||||||
a = str.length,
|
a = str.length,
|
||||||
@ -207,7 +175,7 @@ const getUTCDate = (num = 8) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
||||||
if (typeof (timestamp) !== 'number') return '--'
|
if(typeof(timestamp) !== 'number') return '--'
|
||||||
let date = new Date(timestamp)
|
let date = new Date(timestamp)
|
||||||
let padZero = (num) => String(num).padStart(2, '0')
|
let padZero = (num) => String(num).padStart(2, '0')
|
||||||
let year = date.getFullYear()
|
let year = date.getFullYear()
|
||||||
@ -216,7 +184,7 @@ const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
|||||||
let hours = padZero(date.getHours())
|
let hours = padZero(date.getHours())
|
||||||
let minute = padZero(date.getMinutes())
|
let minute = padZero(date.getMinutes())
|
||||||
let second = padZero(date.getSeconds())
|
let second = padZero(date.getSeconds())
|
||||||
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
let weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六' ]
|
||||||
let week = weekday[date.getDay()]
|
let week = weekday[date.getDay()]
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'date':
|
case 'date':
|
||||||
@ -236,92 +204,12 @@ function resolvePath(dir, path) {
|
|||||||
return path.resolve(dir, path)
|
return path.resolve(dir, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
let shellThrottle = (fn, delay = 1000) => {
|
|
||||||
let timer = null
|
|
||||||
let args = null
|
|
||||||
function throttled() {
|
|
||||||
args = arguments
|
|
||||||
if (!timer) {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
fn(...args)
|
|
||||||
timer = null
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function delayMs() {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, delay))
|
|
||||||
}
|
|
||||||
throttled.last = async () => {
|
|
||||||
await delayMs()
|
|
||||||
fn(...args)
|
|
||||||
}
|
|
||||||
return throttled
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProd = () => {
|
|
||||||
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
|
||||||
return EXEC_ENV === 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
let allowedIPs = process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(',') : ''
|
|
||||||
if (allowedIPs) consola.warn('allowedIPs:', allowedIPs)
|
|
||||||
const isAllowedIp = (requestIP) => {
|
|
||||||
if (allowedIPs.length === 0) return true
|
|
||||||
let flag = allowedIPs.some(item => requestIP.includes(item))
|
|
||||||
if (!flag) consola.warn('requestIP:', requestIP, '不在允许的IP列表中')
|
|
||||||
return flag
|
|
||||||
}
|
|
||||||
|
|
||||||
const ping = (ip, timeout = 5000) => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({ success: false, msg: 'ping timeout!' })
|
|
||||||
}, timeout)
|
|
||||||
let isWin = os.platform() === 'win32'
|
|
||||||
const command = isWin ? `ping -n 1 ${ ip }` : `ping -c 1 ${ ip }`
|
|
||||||
const options = isWin ? { encoding: 'buffer' } : {}
|
|
||||||
|
|
||||||
exec(command, options, (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve({ success: false, msg: 'ping error!' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let output
|
|
||||||
if (isWin) {
|
|
||||||
output = iconv.decode(stdout, 'cp936')
|
|
||||||
} else {
|
|
||||||
output = stdout.toString()
|
|
||||||
}
|
|
||||||
// console.log('output:', output)
|
|
||||||
let match
|
|
||||||
if (isWin) {
|
|
||||||
match = output.match(/平均 = (\d+)ms/)
|
|
||||||
if (!match) {
|
|
||||||
match = output.match(/Average = (\d+)ms/)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match = output.match(/rtt min\/avg\/max\/mdev = [\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+/)
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
resolve({ success: true, time: parseFloat(match[1]) })
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, msg: 'Could not find time in ping output!' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getNetIPInfo,
|
getNetIPInfo,
|
||||||
getLocalNetIP,
|
|
||||||
throwError,
|
throwError,
|
||||||
isIP,
|
isIP,
|
||||||
randomStr,
|
randomStr,
|
||||||
getUTCDate,
|
getUTCDate,
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
resolvePath,
|
resolvePath
|
||||||
shellThrottle,
|
|
||||||
isProd,
|
|
||||||
isAllowedIp,
|
|
||||||
ping
|
|
||||||
}
|
}
|
@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
const { AESDecryptAsync } = require('./encrypt')
|
const { AESDecryptSync } = require('./encrypt')
|
||||||
|
const { readKey } = require('./storage')
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
const { KeyDB } = require('./db-class')
|
|
||||||
const keyDB = new KeyDB().getInstance()
|
|
||||||
|
|
||||||
const enumLoginCode = {
|
const enumLoginCode = {
|
||||||
SUCCESS: 1,
|
SUCCESS: 1,
|
||||||
@ -10,12 +9,12 @@ const enumLoginCode = {
|
|||||||
ERROR_TOKEN: -2
|
ERROR_TOKEN: -2
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验token
|
// 校验token与登录IP
|
||||||
const verifyAuthSync = async (token, clientIp) => {
|
const verifyAuthSync = async (token, clientIp) => {
|
||||||
consola.info('verifyAuthSync IP:', clientIp)
|
consola.info('verifyAuthSync IP:', clientIp)
|
||||||
try {
|
try {
|
||||||
token = await AESDecryptAsync(token) // 先aes解密
|
token = await AESDecryptSync(token) // 先aes解密
|
||||||
const { commonKey } = await keyDB.findOneAsync({})
|
const { commonKey } = await readKey()
|
||||||
const { exp } = jwt.verify(token, commonKey)
|
const { exp } = jwt.verify(token, commonKey)
|
||||||
if (Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
if (Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
||||||
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
|
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
|
||||||
@ -24,6 +23,12 @@ const verifyAuthSync = async (token, clientIp) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
const isProd = () => {
|
||||||
verifyAuthSync
|
const EXEC_ENV = process.env.EXEC_ENV || 'production'
|
||||||
|
return EXEC_ENV === 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyAuthSync,
|
||||||
|
isProd
|
||||||
}
|
}
|
@ -1,4 +1,2 @@
|
|||||||
const consola = require('consola')
|
|
||||||
global.consola = consola
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
require('./app/main.js')
|
require('./app/main.js')
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "3.0.3",
|
"version": "2.0.0",
|
||||||
"description": "easynode-server",
|
"description": "easynode-server",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"local": "cross-env EXEC_ENV=local nodemon index.js",
|
"local": "cross-env EXEC_ENV=local nodemon ./app/index.js",
|
||||||
"prod": "cross-env EXEC_ENV=production nodemon index.js",
|
"prod": "cross-env EXEC_ENV=production nodemon ./app/index.js",
|
||||||
"start": "node ./index.js",
|
"start": "node ./index.js",
|
||||||
"lint": "eslint . --ext .js,.vue",
|
"lint": "eslint . --ext .js,.vue",
|
||||||
"lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix"
|
"lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix"
|
||||||
@ -21,14 +21,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^5.0.0",
|
"@koa/cors": "^5.0.0",
|
||||||
"@seald-io/nedb": "^4.0.4",
|
"@seald-io/nedb": "^4.0.4",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.2",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fs-extra": "^11.2.0",
|
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"iconv-lite": "^0.6.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"koa": "^2.15.3",
|
"koa": "^2.15.3",
|
||||||
"koa-body": "^6.0.1",
|
"koa-body": "^6.0.1",
|
||||||
@ -43,12 +41,9 @@
|
|||||||
"node-os-utils": "^1.3.7",
|
"node-os-utils": "^1.3.7",
|
||||||
"node-rsa": "^1.1.1",
|
"node-rsa": "^1.1.1",
|
||||||
"node-schedule": "^2.1.1",
|
"node-schedule": "^2.1.1",
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
|
||||||
"nodemailer": "^6.9.14",
|
"nodemailer": "^6.9.14",
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"speakeasy": "^2.0.0",
|
|
||||||
"ssh2": "^1.15.0",
|
"ssh2": "^1.15.0",
|
||||||
"ssh2-sftp-client": "^10.0.3"
|
"ssh2-sftp-client": "^10.0.3"
|
||||||
},
|
},
|
||||||
|
@ -34,10 +34,6 @@ module.exports = {
|
|||||||
'vue/singleline-html-element-content-newline': 0,
|
'vue/singleline-html-element-content-newline': 0,
|
||||||
|
|
||||||
// js
|
// js
|
||||||
'space-before-blocks': ['error', 'always',],
|
|
||||||
'space-in-parens': ['error', 'never',],
|
|
||||||
'keyword-spacing': ['error', { 'before': true, 'after': true },],
|
|
||||||
'no-async-promise-executor': 0,
|
|
||||||
'import/no-extraneous-dependencies': 0,
|
'import/no-extraneous-dependencies': 0,
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
<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"
|
<meta name="viewport" content="width=device-width, initial-scale=0,user-scalable=yes">
|
||||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0">
|
|
||||||
<!-- <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/c/font_3309550_x7zmcgwaxf.js"></script>
|
<script src="//at.alicdn.com/t/c/font_3309550_x7zmcgwaxf.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@ -13,8 +10,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
<!-- <script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
|
||||||
<script>eruda.init();</script> -->
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "3.0.3",
|
"version": "0.0.1",
|
||||||
"description": "easynode-web",
|
"description": "easynode-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -29,30 +29,27 @@
|
|||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"csv-parse": "^5.5.6",
|
"csv-parse": "^5.5.6",
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"element-plus": "^2.7.6",
|
"element-plus": "^2.7.6",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0"
|
||||||
"xterm-theme": "^1.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
"code-inspector-plugin": "^0.17.2",
|
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"sass": "^1.77.7",
|
"sass": "^1.77.7",
|
||||||
"unplugin-auto-import": "^0.17.6",
|
"unplugin-auto-import": "^0.17.6",
|
||||||
"unplugin-vue-components": "^0.27.2",
|
"unplugin-vue-components": "^0.27.2",
|
||||||
"vite": "^5.3.6",
|
"vite": "^5.3.3",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue-eslint-parser": "^9.4.3"
|
"vue-eslint-parser": "^9.4.3"
|
||||||
}
|
}
|
||||||
|
0
web/public/upload/.gitkeep
Normal file
@ -19,17 +19,11 @@ export default {
|
|||||||
removeSSH(id) {
|
removeSSH(id) {
|
||||||
return axios({ url: `/remove-ssh/${ id }`, method: 'delete' })
|
return axios({ url: `/remove-ssh/${ id }`, method: 'delete' })
|
||||||
},
|
},
|
||||||
getPlusInfo() {
|
// existSSH(host) {
|
||||||
return axios({ url: '/plus-info', method: 'get' })
|
// return axios({ url: '/exist-ssh', method: 'post', data: { host } })
|
||||||
},
|
// },
|
||||||
getPlusDiscount() {
|
getCommand(host) {
|
||||||
return axios({ url: '/plus-discount', method: 'get' })
|
return axios({ url: '/command', method: 'get', params: { host } })
|
||||||
},
|
|
||||||
getCommand(hostId) {
|
|
||||||
return axios({ url: '/command', method: 'get', params: { hostId } })
|
|
||||||
},
|
|
||||||
decryptPrivateKey(data) {
|
|
||||||
return axios({ url: '/decrypt-private-key', method: 'post', data })
|
|
||||||
},
|
},
|
||||||
getHostList() {
|
getHostList() {
|
||||||
return axios({ url: '/host-list', method: 'get' })
|
return axios({ url: '/host-list', method: 'get' })
|
||||||
@ -40,9 +34,6 @@ export default {
|
|||||||
updateHost(data) {
|
updateHost(data) {
|
||||||
return axios({ url: '/host-save', method: 'put', data })
|
return axios({ url: '/host-save', method: 'put', data })
|
||||||
},
|
},
|
||||||
batchUpdateHost(data) {
|
|
||||||
return axios({ url: '/batch-update-host', method: 'put', data })
|
|
||||||
},
|
|
||||||
removeHost(data) {
|
removeHost(data) {
|
||||||
return axios({ url: '/host-remove', method: 'post', data })
|
return axios({ url: '/host-remove', method: 'post', data })
|
||||||
},
|
},
|
||||||
@ -56,28 +47,28 @@ export default {
|
|||||||
return axios({ url: '/login', method: 'post', data })
|
return axios({ url: '/login', method: 'post', data })
|
||||||
},
|
},
|
||||||
getLoginRecord() {
|
getLoginRecord() {
|
||||||
return axios({ url: '/log', method: 'get' })
|
return axios({ url: '/get-login-record', method: 'get' })
|
||||||
},
|
},
|
||||||
updatePwd(data) {
|
updatePwd(data) {
|
||||||
return axios({ url: '/pwd', method: 'put', data })
|
return axios({ url: '/pwd', method: 'put', data })
|
||||||
},
|
},
|
||||||
getMFA2QR() {
|
// updateHostSort(data) {
|
||||||
return axios({ url: '/mfa2-code', method: 'post' })
|
// return axios({ url: '/host-sort', method: 'put', data })
|
||||||
|
// },
|
||||||
|
getUserEmailList() {
|
||||||
|
return axios({ url: '/user-email', method: 'get' })
|
||||||
},
|
},
|
||||||
getMFA2Status() {
|
getSupportEmailList() {
|
||||||
return axios({ url: '/mfa2-status', method: 'get' })
|
return axios({ url: '/support-email', method: 'get' })
|
||||||
},
|
},
|
||||||
enableMFA2(data) {
|
updateUserEmailList(data) {
|
||||||
return axios({ url: '/mfa2-enable', method: 'post', data })
|
return axios({ url: '/user-email', method: 'post', data })
|
||||||
},
|
},
|
||||||
disableMFA2() {
|
deleteUserEmail(email) {
|
||||||
return axios({ url: '/mfa2-disable', method: 'post' })
|
return axios({ url: `/user-email/${ email }`, method: 'delete' })
|
||||||
},
|
},
|
||||||
getNotifyConfig() {
|
pushTestEmail(data) {
|
||||||
return axios({ url: '/notify-config', method: 'get' })
|
return axios({ url: '/push-email', method: 'post', data })
|
||||||
},
|
|
||||||
updateNotifyConfig(data) {
|
|
||||||
return axios({ url: '/notify-config', method: 'put', data })
|
|
||||||
},
|
},
|
||||||
getNotifyList() {
|
getNotifyList() {
|
||||||
return axios({ url: '/notify', method: 'get' })
|
return axios({ url: '/notify', method: 'get' })
|
||||||
@ -97,14 +88,8 @@ export default {
|
|||||||
deleteGroup(id) {
|
deleteGroup(id) {
|
||||||
return axios({ url: `/group/${ id }`, method: 'delete' })
|
return axios({ url: `/group/${ id }`, method: 'delete' })
|
||||||
},
|
},
|
||||||
getScriptList(params = {}) {
|
getScriptList() {
|
||||||
return axios({ url: '/script', method: 'get', params })
|
return axios({ url: '/script', method: 'get' })
|
||||||
},
|
|
||||||
importScript(data) {
|
|
||||||
return axios({ url: '/import-script', method: 'post', data })
|
|
||||||
},
|
|
||||||
getLocalScriptList() {
|
|
||||||
return axios({ url: '/local-script', method: 'get' })
|
|
||||||
},
|
},
|
||||||
addScript(data) {
|
addScript(data) {
|
||||||
return axios({ url: '/script', method: 'post', data })
|
return axios({ url: '/script', method: 'post', data })
|
||||||
@ -114,23 +99,5 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteScript(id) {
|
deleteScript(id) {
|
||||||
return axios({ url: `/script/${ id }`, method: 'delete' })
|
return axios({ url: `/script/${ id }`, method: 'delete' })
|
||||||
},
|
|
||||||
batchRemoveScript(data) {
|
|
||||||
return axios({ url: '/batch-remove-script', method: 'post', data })
|
|
||||||
},
|
|
||||||
getOnekeyRecord() {
|
|
||||||
return axios({ url: '/onekey', method: 'get' })
|
|
||||||
},
|
|
||||||
deleteOnekeyRecord(ids) {
|
|
||||||
return axios({ url: '/onekey', method: 'post', data: { ids } })
|
|
||||||
},
|
|
||||||
getEasynodeVersion() {
|
|
||||||
return axios({ url: '/version', method: 'get' })
|
|
||||||
},
|
|
||||||
getPlusConf() {
|
|
||||||
return axios({ url: '/plus-conf', method: 'get' })
|
|
||||||
},
|
|
||||||
updatePlusKey(data) {
|
|
||||||
return axios({ url: '/plus-conf', method: 'post', data })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import { ref, getCurrentInstance } from 'vue'
|
|
||||||
const { proxy: { $store } } = getCurrentInstance()
|
|
||||||
|
|
||||||
const locale = ref(zhCn)
|
const locale = ref(zhCn)
|
||||||
$store.setDefaultTheme()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 9.9 KiB |
18
web/src/assets/scss/animate.scss
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// vue transition 动画
|
||||||
|
.list-move, /* apply transition to moving elements */
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ensure leaving items are taken out of layout flow so that moving
|
||||||
|
animations can be calculated correctly. */
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
22
web/src/assets/scss/element-ui.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// element css bug
|
||||||
|
.el-notification__content {
|
||||||
|
text-align: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-date-editor {
|
||||||
|
--el-date-editor-width: 100%;
|
||||||
|
}
|
||||||
|
.el-input__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-scroll .el-tabs__nav {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.el-tabs__content {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// :root {
|
||||||
|
// --active-color: red;
|
||||||
|
// }
|
@ -1,88 +0,0 @@
|
|||||||
// $--colors: (
|
|
||||||
// "primary": (
|
|
||||||
// "base": #589ef8,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// @forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
|
||||||
// $colors: $--colors
|
|
||||||
// );
|
|
||||||
/** element内置黑暗主题 */
|
|
||||||
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
|
||||||
|
|
||||||
/** 自定义黑暗主题 */
|
|
||||||
html.dark {
|
|
||||||
// * admin
|
|
||||||
--bg-color: #000;
|
|
||||||
--main-bg-color: #181818;
|
|
||||||
--v-border-light: 1px solid #4c4c4d;
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login_container {
|
|
||||||
// background: rgba(171, 181, 196, 0.3);
|
|
||||||
.login_box {
|
|
||||||
background-color: var(--main-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.top_bar_container {
|
|
||||||
background-color: var(--main-bg-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.router_box {
|
|
||||||
background-color: var(--main-bg-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aside_container {
|
|
||||||
background-color: var(--main-bg-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal_top {
|
|
||||||
border-bottom: 1px solid #454242;
|
|
||||||
}
|
|
||||||
.sftp_tab_container {
|
|
||||||
background-color: var(--main-bg-color) !important;
|
|
||||||
.dir-list {
|
|
||||||
.active {
|
|
||||||
background-color: #454242 !important;
|
|
||||||
}
|
|
||||||
li:hover {
|
|
||||||
background-color: #454242 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
.left {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.info_box {
|
|
||||||
border-right: 1px solid #454242;
|
|
||||||
.el-progress-bar__innerText {
|
|
||||||
span {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll-bar
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #5c5c5c;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all .2s ease-in-out;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #6d6d6d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
.el-menu-item:not(.is-active):hover {
|
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
// $--colors: (
|
|
||||||
// "primary": (
|
|
||||||
// "base": green,
|
|
||||||
// ),
|
|
||||||
// "success": (
|
|
||||||
// "base": #21ba45,
|
|
||||||
// ),
|
|
||||||
// "warning": (
|
|
||||||
// "base": #f2711c,
|
|
||||||
// ),
|
|
||||||
// "danger": (
|
|
||||||
// "base": #db2828,
|
|
||||||
// ),
|
|
||||||
// "error": (
|
|
||||||
// "base": #db2828,
|
|
||||||
// ),
|
|
||||||
// "info": (
|
|
||||||
// "base": #42b8dd,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// You should use them in scss, because we calculate it by sass.
|
|
||||||
// comment next lines to use default color
|
|
||||||
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
|
|
||||||
// // do not use same name, it will override.
|
|
||||||
// // $colors: $--colors,
|
|
||||||
// // $button-padding-horizontal: ("default": 50px)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if you want to import all
|
|
||||||
// @use "element-plus/theme-chalk/src/index.scss" as *;
|
|
||||||
|
|
||||||
// You can comment it to hide debug info.
|
|
||||||
// @debug $--colors;
|
|
||||||
|
|
||||||
// custom dark variables
|
|
||||||
@use "./dark.scss";
|
|
@ -1,52 +1,39 @@
|
|||||||
html {
|
// 滚动条
|
||||||
font-size: 15px;
|
html, body, div, ul, section, textarea {
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
div,
|
|
||||||
ul,
|
|
||||||
section,
|
|
||||||
textarea {
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
// 滚动条整体部分
|
// 滚动条整体部分
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
height: 8px;
|
||||||
height: 6px;
|
width: 2px;
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底层轨道
|
// 底层轨道
|
||||||
// &::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
// background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
// border-radius: 3px;
|
border-radius: 10px;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 滚动滑块
|
// 滚动滑块
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: #0003;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all .2s ease-in-out;
|
// background-color: #1989fa;
|
||||||
|
background-image: -webkit-gradient(linear, 40% 0%, 75% 84%, from(#a18cd1), to(#fbc2eb), color-stop(.6, #54DE5D));
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
cursor: pointer;
|
background-color: #067ef7;
|
||||||
background-color: #0000004d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::view-transition-new(root),
|
|
||||||
::view-transition-old(root) {
|
|
||||||
/* 关闭默认动画,否则影响自定义动画的执行 */
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局背景
|
// 全局背景
|
||||||
body {
|
body {
|
||||||
|
// background-position: center center;
|
||||||
|
// background-attachment: fixed;
|
||||||
|
// background-size: cover;
|
||||||
|
// background-repeat: no-repeat;
|
||||||
|
// // background-image: url(../bg.jpg), linear-gradient(to bottom, #010179, #F5C4C1, #151799);
|
||||||
background-color: #E7EBF4;
|
background-color: #E7EBF4;
|
||||||
background-image: url(https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg);
|
background-image: url(https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@ -54,7 +41,8 @@ body {
|
|||||||
background-size: 58%;
|
background-size: 58%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
html, body {
|
||||||
color: var(--el-color-primary);
|
// min-width: 1200px;
|
||||||
cursor: pointer;
|
// height: 100vh;
|
||||||
|
// overflow: hidden;
|
||||||
}
|
}
|
@ -1,87 +0,0 @@
|
|||||||
.mobile_menu_btn {
|
|
||||||
margin-right: auto;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile_menu_drawer {
|
|
||||||
width: auto !important;
|
|
||||||
.mobile_logo_wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
img {
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (min-width: 969px) {
|
|
||||||
[class^="mobile_"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 968px) {
|
|
||||||
.login_box {
|
|
||||||
width: 86vw!important;
|
|
||||||
}
|
|
||||||
.view_container {
|
|
||||||
.aside_container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.top_bar_container {
|
|
||||||
width: 100%;
|
|
||||||
.bar_wrap {
|
|
||||||
h2 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.terminal_container {
|
|
||||||
.terminal_link_tips {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.terminal_wrap {
|
|
||||||
.terminal_and_sftp_wrap {
|
|
||||||
flex: auto;
|
|
||||||
.sftp_tab_container {
|
|
||||||
section {
|
|
||||||
.left {
|
|
||||||
min-width: 150px;
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
.filter_input {
|
|
||||||
width: auto;
|
|
||||||
min-width: auto;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
.path {
|
|
||||||
display: inline-block;
|
|
||||||
padding-right: 15px;
|
|
||||||
}
|
|
||||||
.path_input {
|
|
||||||
width: auto;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-dialog {
|
|
||||||
--el-dialog-width: 94%!important;
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 501 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 177 KiB |
Before Width: | Height: | Size: 23 KiB |