mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-10 01:49:00 -06:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eb4a4ea62 | ||
|
|
9caf34e641 | ||
|
|
b92bb0c1cb | ||
|
|
7da5194e90 | ||
|
|
b8c34d04bf | ||
|
|
ab3d980c4d | ||
|
|
a32130348d | ||
|
|
74917e321f | ||
|
|
07c3c5b61f | ||
|
|
a0862429b9 | ||
|
|
f36ce20df9 | ||
|
|
fdd4a9c6d1 | ||
|
|
840341e331 | ||
|
|
c9310a319a | ||
|
|
ae204b418f | ||
|
|
97a9b376c7 | ||
|
|
54515f6b96 | ||
|
|
cffb85de49 | ||
|
|
c0d9d167a7 | ||
|
|
cef6a141c1 | ||
|
|
29fd3ba9d1 | ||
|
|
7a9affced2 | ||
|
|
83a65559fb | ||
|
|
68913d2185 | ||
|
|
901a0380d7 | ||
|
|
1db304a5f3 | ||
|
|
7fe3447a83 | ||
|
|
85f6447a16 | ||
|
|
69243e5ebb | ||
|
|
d7ca7c453d | ||
|
|
d5eb644baa | ||
|
|
26078fe46e | ||
|
|
93ce6009c6 | ||
|
|
2cae9e4336 | ||
|
|
097078f93c | ||
|
|
52937df57d | ||
|
|
5a39562f2d | ||
|
|
a09ed2dc1f | ||
|
|
3bd52590ff | ||
|
|
fceac7d3b1 | ||
|
|
0ffc78a747 |
39
README.md
39
README.md
@@ -3,25 +3,28 @@ NanoKVM
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html"> Quick Start </a> |
|
||||
<a href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html"> Quick Start </a>
|
||||
|
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> Hardware </a>
|
||||
</h3>
|
||||
|
||||
English | [中文](./README_ZH.md)
|
||||
English | [中文](./README_ZH.md) | [日本語](./README_JA.md)
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> If you have any issues or suggestions, creating issue here, or tell us with [MaixHub Discussion](https://maixhub.com/discussion/nanokvm).
|
||||
|
||||
## Opensource & Contribution & Shipping
|
||||
## Opensource & Contribution
|
||||
|
||||
The star is coming to 2K! We will organize and opensource backend source code in Mid October!(and it is also the time all preoreders (before 10.1) ship out)
|
||||
### Update 2025.02.05
|
||||
For users concerned about security, you can read this issue: https://github.com/sipeed/NanoKVM/issues/301 , you will find explanations addressing all security concerns.
|
||||
GitHub, as an open and transparent platform, provides a space for discussing so-called "backdoor" concerns. Open-source makes products more secure!
|
||||
|
||||
### Update 10.18
|
||||
### Update 2024.10.18
|
||||
|
||||
We have opensourced the backend code in 10.8, and thank you for Civil PR it immediately, we will give you newest NanoKVM-PCIe as a reward!
|
||||
The mid-Oct batch is starting testing and packaging, most order before 10.1 will ship out next and next next week.
|
||||
@@ -70,12 +73,34 @@ The NanoKVM Lite is constructed by the LicheeRV Nano and HDMItoCSI board, and th
|
||||
|
||||
The NanoKVM image is built on LicheeRV Nano SDK and MaixCDK, and is compatible with materials that use the LicheeRV Nano, opposite the KVM software cannot be used with the LicheeRV Nano or other SG2002 products. If you would like to build an HDMI input application on LicheeRV Nano or MaixCam, please contact us for technical support.
|
||||
|
||||
Note: Out of the 256MB memory in SG2002, 105MB is currently allocated for the multimedia subsystem, which NanoKVM will use for video image acquisition and processing
|
||||
Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the multimedia subsystem, which NanoKVM will use for video image acquisition and processing
|
||||
|
||||
+ [NanoKVM-A Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
+ [NanoKVM-B Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf)
|
||||
+ [NanoKVM img](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Q1 2025
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- [ ] Refactor the HDMI module and remove the libmaixcam_lib.so dependency
|
||||
- [ ] Disable SSH by default and add an enable/disable toggle in the web UI
|
||||
- [ ] Disable Tailscale by default and add an enable/disable toggle in the web UI
|
||||
- [ ] Add JWT configuration options to the configuration file
|
||||
|
||||
**Distribution:**
|
||||
|
||||
- [ ] Distribute applications via GitHub
|
||||
- [ ] Implement integrity checks for online updates
|
||||
- [ ] Support offline updates
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
- [ ] Fix the DNS issue
|
||||
- [ ] Fix the CSRF vulnerability
|
||||
|
||||
## Where to buy
|
||||
|
||||
* [Aliexpress(global except USA&Russia)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
|
||||
87
README_JA.md
Normal file
87
README_JA.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# NanoKVM
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html"> クイックスタート </a> |
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> ハードウェア </a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | [中文](./README_ZH.md) | 日本語
|
||||
|
||||
</div>
|
||||
|
||||
> Your NanoKVM Power by RISC-V !
|
||||
> 使用中に問題や提案がある場合は、ここで issue を提出するか、[MaixHub Discussion](https://maixhub.com/discussion/nanokvm) でお知らせください。
|
||||
|
||||
## オープンソース & 貢献 & 出荷
|
||||
|
||||
スターが2Kに近づいています!10月中旬にバックエンドのソースコードを整理してオープンソース化します!(また、10.1前のすべての予約注文が出荷される時期でもあります)
|
||||
|
||||
### 10.18 更新
|
||||
|
||||
10.8にバックエンドコードをオープンソース化しました。すぐにPRを提出してくれたCivilに感謝します。最新のNanoKVM-PCIeを報酬として提供します!
|
||||
10月中旬のバッチはテストとパッケージングを開始しており、10.1前のほとんどの注文は次週と次々週に出荷されます。
|
||||
10.1以降、アリエクスプレスの出荷日は12月に変更されました。これは保守的な納期です。10月の注文は11.15〜12.15の間に発送される予定です。
|
||||
|
||||
## 紹介
|
||||
|
||||
Lichee NanoKVMは、LicheeRV NanoをベースにしたIP-KVM製品で、LicheeRV Nanoの極小サイズと強力な機能を継承しています。
|
||||
Lichee NanoKVMは2つのバージョンがあります:
|
||||
NanoKVM Liteは基本構成で、一定のDIY能力を持つ個人ユーザーや大量のニーズを持つ企業ユーザーに適しています。
|
||||
NanoKVM Fullは完全版で、洗練されたケースと完全なアクセサリーを備え、起動時にすぐに使用できるシステムミラーカードが内蔵されており、個人ユーザーに推奨されます。
|
||||
|
||||

|
||||
|
||||
## 技術仕様
|
||||
|
||||
| 製品 | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 |
|
||||
| --- | --- | --- | --- |
|
||||
| 計算ユニット | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
|
||||
| 解像度 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
|
||||
| ビデオエンコーディング | MJPEG, H264(開発中) | MJPEG, H264(開発中) | MJPEG, H264 |
|
||||
| ビデオ遅延 | 90~230ms | 90~230ms | 100~230ms |
|
||||
| UEFI/BIOS | ✓ | ✓ | ✓ |
|
||||
| 仮想HID | ✓ | ✓ | ✓ |
|
||||
| 仮想CD-ROM | ✓ | ✓ | ✓ |
|
||||
| IPMI | ✓ | ✓ | ✓ |
|
||||
| Wake-on-LAN | ✓ | ✓ | ✓ |
|
||||
| ETH | 100M/10M | 100M/10M | 1000M/100M/10M |
|
||||
| ATX電源制御 | なし、ユーザーが自分で接続 | USBインターフェースIO制御ボード | RJ-45インターフェースIO制御ボード |
|
||||
| OLED | なし、ユーザーが自分で接続 | 128x64 0.96" 白 | 128x32 0.91" 白 |
|
||||
| UART | 2 | 2 | 1 |
|
||||
| TFカード | なし | ✓ | ✓ |
|
||||
| 拡張 | なし | PoE | WiFi/LTE |
|
||||
| 消費電力 | 0.2A@5V | 0.2A@5V | ピーク2.6A@5V |
|
||||
| 電源入力 | PC USBで給電可能 | PC USBまたは補助電源 | DC 5V 3A電源が必要 |
|
||||
| 冷却 | 静音ファンレス | 静音ファンレス | ファン冷却 |
|
||||
| サイズ | 23x37x15mm <br> ~1/30 PiKVM V4サイズ | 40x36x36mm <br/> ~1/7 PiKVM V4サイズ | 120x68x44mm |
|
||||
|
||||

|
||||
|
||||
## ハードウェアプラットフォーム NanoKVM
|
||||
|
||||
NanoKVMはSipeed [LicheeRV Nano](https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html)に基づいており、仕様、回路図、寸法図は[こちら](http://cn.dl.sipeed.com/shareURL/LICHEE/LicheeRV_Nano)で確認できます。
|
||||
|
||||
NanoKVM LiteはLicheeRV NanoとHDMItoCSIボードで構成され、NanoKVM FULLはNanoKVM LiteにNanoKVM-A/Bボードとシェルを追加しています。HDMItoCSIボードはHDMI信号を変換するために使用されます。NanoKVM-AにはOLED、ATX制御出力(USB Type-Cインターフェース)、補助電源供給およびATX電源オン/オフおよびリセットボタンが含まれています。NanoKVM-Bは一端がAボードに接続され、他端がコンピュータのATXピンに接続され、コンピュータの電源をリモートで制御します。
|
||||
|
||||
NanoKVMイメージはLicheeRV Nano SDKおよびMaixCDKに基づいて構築されており、LicheeRV Nanoを使用する資料と互換性があります。逆に、KVMソフトウェアはLicheeRV Nanoや他のSG2002製品では使用できません。LicheeRV NanoやMaixCamでHDMI入力アプリケーションを構築したい場合は、技術サポートを受けるためにお問い合わせください。
|
||||
|
||||
注:SG2002の256MBメモリのうち、現在105MBがマルチメディアサブシステムに割り当てられており、NanoKVMはビデオ画像の取得と処理にこのメモリを使用します。
|
||||
|
||||
+ [NanoKVM-A 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf)
|
||||
+ [NanoKVM-B 回路図](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf)
|
||||
+ [NanoKVM イメージダウンロード](https://github.com/sipeed/NanoKVM/releases/tag/NanoKVM)
|
||||
|
||||
## 購入方法
|
||||
|
||||
* [Aliexpress(グローバル、米国とロシアを除く)](https://www.aliexpress.com/item/1005007369816019.html)
|
||||
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
|
||||
* [予約注文 (淘宝とアリエクスプレスがサポートされていない国)](https://sipeed.com/nanokvm)
|
||||
|
||||
## コミュニティ
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQグループ: 703230713
|
||||
@@ -3,14 +3,14 @@ NanoKVM
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
<h3>
|
||||
<a href="https://wiki.sipeed.com/hardware/zh/lichee/RV_Nano/1_intro.html"> 快速开始 </a> |
|
||||
<a href="https://cn.dl.sipeed.com/shareURL/KVM/nanoKVM"> 硬件资料 </a>
|
||||
</h3>
|
||||
|
||||
[English](./README.md) | 中文
|
||||
[English](./README.md) | 中文 | [日本語](./README_JA.md)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -78,6 +78,3 @@ NanoKVM 镜像在LicheeRV Nano SDK 和 MaixCDK 基础上构建,可以兼容使
|
||||
|
||||
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
|
||||
* QQ group: 703230713
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,98 @@
|
||||
# NanoKVM Server
|
||||
|
||||
The code of NanoKVM backend server. For more documentation, please refer to [Wiki](https://wiki.sipeed.com/nanokvm).
|
||||
This is the backend server implementation for NanoKVM.
|
||||
|
||||
For detailed documentation, please visit our [Wiki](https://wiki.sipeed.com/nanokvm).
|
||||
|
||||
## Structure
|
||||
|
||||
```shell
|
||||
server
|
||||
├── config // server config
|
||||
├── logger // server log
|
||||
├── main.go
|
||||
├── middleware // server middleware
|
||||
├── proto // api request and response arguments
|
||||
├── router // api routes
|
||||
├── service // api handlers
|
||||
└── utils // utils functions
|
||||
├── common // Common utility components
|
||||
├── config // Server configuration
|
||||
├── dl_lib // Shared object libraries
|
||||
├── include // Header files for shared objects
|
||||
├── logger // Logging system
|
||||
├── middleware // Server middleware components
|
||||
├── proto // API request/response definitions
|
||||
├── router // API route handlers
|
||||
├── service // Core service implementations
|
||||
├── utils // Utility functions
|
||||
└── main.go
|
||||
```
|
||||
|
||||
## Development
|
||||
## Configuration
|
||||
|
||||
The configuration file path is `/etc/kvm/server.yaml`. There are two configurable options:
|
||||
The configuration file path is `/etc/kvm/server.yaml`.
|
||||
|
||||
- `log: error`: log level, default is `error`. Too many logs may affect service performance, it is recommended to use the `error` level in production environment.
|
||||
```yaml
|
||||
proto: http
|
||||
port:
|
||||
http: 80
|
||||
https: 443
|
||||
cert:
|
||||
crt: server.crt
|
||||
key: server.key
|
||||
|
||||
- `authentication: enable`: Whether to enable authentication, default is `enable`. Logging in is required after each service restart, use `disable` will skip the authentication check and don't need to log in again. Please delete this configuration for production environments!
|
||||
# Log level (debug/info/warn/error)
|
||||
# Note: Use 'info' or 'error' in production, 'debug' only for development
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
# Authentication setting (enable/disable)
|
||||
# Note: Only disable authentication in development environment
|
||||
authentication: enable
|
||||
|
||||
## Deployment
|
||||
jwt:
|
||||
# JWT secret key. If left empty, a random 64-byte key will be generated automatically.
|
||||
secretKey: ""
|
||||
# JWT token expiration time in seconds. Default: 2678400 (31 days)
|
||||
refreshTokenDuration: 2678400
|
||||
# Invalidate all JWT tokens when the user logs out. Default: true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
Work in progress.
|
||||
# Address for custom STUN server
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
The project requires CGO. It must be compiled on Linux with a toolchain installed. More details will be added later.
|
||||
# Address and authentication for custom TURN server
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
|
||||
## Compile & Deploy
|
||||
|
||||
Note: Use Linux operating system (x86-64). This build process is not compatible with ARM, Windows or macOS.
|
||||
|
||||
1. Install the Toolchain
|
||||
1. Download the toolchain from the following link: [Download Link](https://sophon-file.sophon.cn/sophon-prod-s3/drive/23/03/07/16/host-tools.tar.gz).
|
||||
2. Extract the file and add the `host-tools/gcc/riscv64-linux-musl-x86_64/bin` directory to your PATH environment variable.
|
||||
3. Run `riscv64-unknown-linux-musl-gcc -v`. If there is version information in the output, the installation is successful.
|
||||
|
||||
2. Compile the Project
|
||||
1. Run `cd server` from the project root directory.
|
||||
2. Run `go mod tidy` to install Go dependencies.
|
||||
3. Run `CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build` to compile the project.
|
||||
4. After compilation, an executable file named `NanoKVM-Server` will be generated.
|
||||
|
||||
3. Modify RPATH
|
||||
1. Run `sudo apt install patchelf` or `pip install patchelf` to install patchelf.
|
||||
2. Run `patchelf --version`. Ensure the version is 0.14 or higher`.
|
||||
3. Run `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` to modify the RPATH of the executable file.
|
||||
|
||||
4. Deploy the Application
|
||||
1. File uploads requires SSH. Please enable it in the Web Settings: `Settings > SSH`;
|
||||
2. Replace the original file in the NanoKVM `/kvmapp/server/` directory with the newly compiled `NanoKVM-Server`.
|
||||
3. Restart the service on NanoKVM by executing `/etc/init.d/S95nanokvm restart`.
|
||||
|
||||
## Manually Update
|
||||
|
||||
> File uploads requires SSH. Please enable it in the Web Settings: `Settings > SSH`;
|
||||
|
||||
1. Download the latest application from [GitHub](https://github.com/sipeed/NanoKVM/releases);
|
||||
2. Unzip the downloaded file and rename the unzipped folder to `kvmapp`;
|
||||
3. Back up the existing `/kvmapp` directory on your NanoKVM, then replace it with the new `kvmapp` folder;
|
||||
4. Run `/etc/init.d/S95nanokvm restart` on your NanoKVM to restart the service.
|
||||
|
||||
82
server/README_JA.md
Normal file
82
server/README_JA.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# NanoKVM サーバー
|
||||
|
||||
これは NanoKVM のバックエンドサーバーの実装です。
|
||||
|
||||
詳細なドキュメントについては、[Wiki](https://wiki.sipeed.com/nanokvm) を参照してください。
|
||||
|
||||
## 構造
|
||||
|
||||
```shell
|
||||
server
|
||||
├── common // 共通ユーティリティコンポーネント
|
||||
├── config // サーバー設定
|
||||
├── dl_lib // 共有オブジェクトライブラリ
|
||||
├── include // 共有オブジェクトのヘッダーファイル
|
||||
├── logger // ロギングシステム
|
||||
├── middleware // サーバーミドルウェアコンポーネント
|
||||
├── proto // API リクエスト/レスポンス定義
|
||||
├── router // API ルートハンドラ
|
||||
├── service // コアサービスの実<E381AE><E5AE9F>
|
||||
├── utils // ユーティリティ関数
|
||||
└── main.go
|
||||
```
|
||||
|
||||
## 設定
|
||||
|
||||
設定ファイルのパスは `/etc/kvm/server.yaml` です。
|
||||
|
||||
```yaml
|
||||
proto: http
|
||||
port:
|
||||
http: 80
|
||||
https: 443
|
||||
cert:
|
||||
crt: server.crt
|
||||
key: server.key
|
||||
|
||||
# ログレベル (debug/info/warn/error)
|
||||
# 注意: 本番環境では 'info' または 'error' を使用し、'debug' は開発環境でのみ使用してください
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
# 認証設定 (enable/disable)
|
||||
# 注意: 認証を無効にするのは開発環境でのみ行ってください
|
||||
authentication: enable
|
||||
|
||||
# JWT 秘密鍵の設定
|
||||
# 空のままにすると、サーバー起動時にランダムな鍵が生成されます
|
||||
secretKey: ""
|
||||
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
## コンパイルとデプロイ
|
||||
|
||||
注意: Linux オペレーティングシステム (x86-64) を使用してください。このビルドプロセスは ARM、Windows、macOS では互換性がありません。
|
||||
|
||||
1. ツールチェーンのインストール
|
||||
1. 以下のリンクからツールチェーンをダウンロードします: [ダウンロードリンク](https://sophon-file.sophon.cn/sophon-prod-s3/drive/23/03/07/16/host-tools.tar.gz)。
|
||||
2. ファイルを解凍し、`host-tools/gcc/riscv64-linux-musl-x86_64/bin` ディレクトリを PATH 環境変数に追加します。
|
||||
3. `riscv64-unknown-linux-musl-gcc -v` を実行します。バージョン情報が表示されれば、インストールは成功です。
|
||||
|
||||
2. プロジェクトのコンパイル
|
||||
1. プロジェクトのルートディレクトリから `cd server` を実行します。
|
||||
2. `go mod tidy` を実行して Go の依存関係をインストールします。
|
||||
3. `CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build` を実行してプロジェクトをコンパイルします。
|
||||
4. コンパイルが完了すると、`NanoKVM-Server` という名前の実行ファイルが生成されます。
|
||||
|
||||
3. RPATH の変更
|
||||
1. `sudo apt install patchelf` または `pip install patchelf` を実行して patchelf をインストールします。
|
||||
2. `patchelf --version` を実行します。バージョンが 0.14 以上であることを確認します。
|
||||
3. `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` を実行して、実行ファイルの RPATH を変更します。
|
||||
|
||||
4. アプリケーションのデプロイ
|
||||
1. デプロイ前に、ブラウザでアプリケーションを最新バージョンに更新します。手順は[こちら](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/updating.html)を参照してください。
|
||||
2. コンパイルして生成された `NanoKVM-Server` ファイルを使用して、NanoKVM の `/kvmapp/server/` ディレクトリ内の元のファイルを置き換えます。
|
||||
3. NanoKVM で `/etc/init.d/S95nanokvm restart` を実行してサービスを再起動します。
|
||||
@@ -6,25 +6,88 @@ NanoKVM 后端服务的代码。更多文档请参考 [Wiki](https://wiki.sipeed
|
||||
|
||||
```shell
|
||||
server
|
||||
├── common // 公用组件
|
||||
├── config // 服务配置
|
||||
├── dl_lib // so 文件
|
||||
├── include // 头文件
|
||||
├── logger // 服务日志
|
||||
├── main.go
|
||||
├── middleware // 中间件
|
||||
├── proto // api 请求响应参数
|
||||
├── router // api 路由
|
||||
├── service // api 处理逻辑
|
||||
└── utils // 工具函数
|
||||
├── utils // 工具函数
|
||||
└── main.go
|
||||
```
|
||||
|
||||
## 开发
|
||||
## 配置文件
|
||||
|
||||
配置文件路径为 `/etc/kvm/server.yaml`。有两个可配置的选项:
|
||||
配置文件路径为 `/etc/kvm/server.yaml`。
|
||||
|
||||
- `log: error`:日志等级,默认为 `error`。日志过多可能会影响服务性能,生产环境中建议使用 `error` 等级。
|
||||
- `authentication: enable`:是否启用鉴权,默认开启。服务每次重启后都需要重新登录,将该选项设为 `disable` 会跳过鉴权检查,可以不用重复登录。生产环境请删除该条配置!
|
||||
```yaml
|
||||
proto: http
|
||||
port:
|
||||
http: 80
|
||||
https: 443
|
||||
cert:
|
||||
crt: server.crt
|
||||
key: server.key
|
||||
|
||||
# 日志级别(debug/info/warn/error)
|
||||
# 注意:在生产环境中使用 info 或 error。debug 模式仅在开发环境中使用。
|
||||
logger:
|
||||
level: info
|
||||
file: stdout
|
||||
|
||||
# 鉴权设置(enable/disable)
|
||||
# 注意:生产环境中请勿使用 disable。
|
||||
authentication: enable
|
||||
|
||||
## 部署
|
||||
jwt:
|
||||
# jwt 密钥。设置为空则使用随机生成的64位密钥
|
||||
secretKey: ""
|
||||
# jwt token 过期时间(单位:秒),默认为2678400(31天)
|
||||
refreshTokenDuration: 2678400
|
||||
# 在帐号登出时是否使所有 jwt token 失效。默认为 true
|
||||
revokeTokensOnLogout: true
|
||||
|
||||
待完善。
|
||||
stun: stun.l.google.com:19302
|
||||
|
||||
项目中使用了 CGO,需要使用 Linux 并安装工具链后才能编译。这部分内容会晚一点更新。
|
||||
turn:
|
||||
turnAddr: turn.cloudflare.com:3478
|
||||
turnUser: example_user
|
||||
turnCred: example_cred
|
||||
```
|
||||
|
||||
## 编译部署
|
||||
|
||||
**注意:请使用 Linux 操作系统(x86-64)。该工具链无法在 ARM、Windows 或 macOS 下使用。**
|
||||
|
||||
1. 安装工具链
|
||||
1. 下载工具链:[下载地址](https://sophon-file.sophon.cn/sophon-prod-s3/drive/23/03/07/16/host-tools.tar.gz);
|
||||
2. 解压下载文件,然后将 `host-tools/gcc/riscv64-linux-musl-x86_64/bin` 目录加入到环境变量;
|
||||
3. 执行 `riscv64-unknown-linux-musl-gcc -v`,如果显示版本信息则安装成功。
|
||||
|
||||
2. 编译
|
||||
1. 在项目根目录下执行 `cd server` 进入 server 目录;
|
||||
2. 执行 `go mod tidy` 安装 Go 依赖包;
|
||||
3. 执行 `CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build` 进行编译;
|
||||
4. 编译完成后,会生成可执行文件 `NanoKVM-Server`。
|
||||
|
||||
3. 修改 RPATH
|
||||
1. 执行 `sudo apt install patchelf` 或 `pip install patchelf` 安装 patchelf;
|
||||
2. 执行 `patchelf --version`,确保版本大于等于 0.14;
|
||||
3. 执行 `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` 修改可执行文件的 RPATH 属性。
|
||||
|
||||
4. 部署
|
||||
1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用;
|
||||
2. 使用编译生成的 `NanoKVM-Server` 文件,替换 NanoKVM 中 `/kvmapp/server/` 目录下的原始文件;
|
||||
3. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。
|
||||
|
||||
## 手动更新
|
||||
|
||||
> 请确保已经在 Web 界面的 `设置 - SSH` 中启用了 SSH 功能,以便上传文件。
|
||||
|
||||
1. 从 [GitHub](https://github.com/sipeed/NanoKVM/releases) 下载最新的应用安装包;
|
||||
2. 解压缩下载的安装包,并将解压后的文件夹重命名为 `kvmapp`;
|
||||
3. 备份 NanoKVM 系统中的 `/kvmapp` 目录,然后用解压后的 `kvmapp` 文件夹替换现有目录。
|
||||
4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。
|
||||
@@ -112,15 +112,15 @@ func (k *KvmVision) ReadH264SPS() ([]byte, int) {
|
||||
dataSize C.uint32_t
|
||||
)
|
||||
|
||||
result := C.kvmv_get_sps_frame(&kvmData, &dataSize)
|
||||
result := int(C.kvmv_get_sps_frame(&kvmData, &dataSize))
|
||||
if result < 0 {
|
||||
log.Errorf("failed to read sps: %v", result)
|
||||
return nil, int(result)
|
||||
return nil, result
|
||||
}
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
|
||||
return data, int(result)
|
||||
return data, result
|
||||
}
|
||||
|
||||
func (k *KvmVision) ReadH264PPS() ([]byte, int) {
|
||||
@@ -129,14 +129,29 @@ func (k *KvmVision) ReadH264PPS() ([]byte, int) {
|
||||
dataSize C.uint32_t
|
||||
)
|
||||
|
||||
result := C.kvmv_get_pps_frame(&kvmData, &dataSize)
|
||||
result := int(C.kvmv_get_pps_frame(&kvmData, &dataSize))
|
||||
if result < 0 {
|
||||
log.Errorf("failed to read pps: %v", result)
|
||||
return nil, int(result)
|
||||
return nil, result
|
||||
}
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(kvmData), C.int(dataSize))
|
||||
return data, int(result)
|
||||
return data, result
|
||||
}
|
||||
|
||||
func (k *KvmVision) SetHDMI(enable bool) int {
|
||||
hdmiEnable := C.uint8_t(0)
|
||||
if enable {
|
||||
hdmiEnable = C.uint8_t(1)
|
||||
}
|
||||
|
||||
result := int(C.kvmv_hdmi_control(hdmiEnable))
|
||||
if result < 0 {
|
||||
log.Errorf("failed to set hdmi to %t", enable)
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (k *KvmVision) Close() {
|
||||
|
||||
@@ -2,28 +2,24 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
config Config
|
||||
once sync.Once
|
||||
instance Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func GetInstance() *Config {
|
||||
once.Do(initialize)
|
||||
|
||||
return &config
|
||||
return &instance
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
@@ -43,20 +39,16 @@ func initialize() {
|
||||
log.Fatalf("Failed to validate configuration!")
|
||||
}
|
||||
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
if err := viper.Unmarshal(&instance); err != nil {
|
||||
log.Fatalf("Failed to parse configuration: %s", err)
|
||||
}
|
||||
|
||||
if config.Authentication == "disable" {
|
||||
checkDefaultValue()
|
||||
|
||||
if instance.Authentication == "disable" {
|
||||
log.Println("NOTICE: Authentication is disabled! Please ensure your service is secure!")
|
||||
}
|
||||
|
||||
if config.SecretKey == "" {
|
||||
config.SecretKey = generateRandomString()
|
||||
}
|
||||
|
||||
config.Hardware = getHardware()
|
||||
|
||||
log.Println("config loaded successfully")
|
||||
}
|
||||
|
||||
@@ -128,16 +120,3 @@ func validate() error {
|
||||
|
||||
return readByDefault()
|
||||
}
|
||||
|
||||
// Generate random string for secret key.
|
||||
func generateRandomString() string {
|
||||
b := make([]byte, 64)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
currentTime := time.Now().UnixNano()
|
||||
timeString := fmt.Sprintf("%d", currentTime)
|
||||
return fmt.Sprintf("%064s", timeString)
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package config
|
||||
|
||||
var defaultConfig = &Config{
|
||||
Protocol: "http",
|
||||
Proto: "http",
|
||||
Port: Port{
|
||||
Http: 80,
|
||||
Https: 443,
|
||||
@@ -14,5 +14,37 @@ var defaultConfig = &Config{
|
||||
Level: "info",
|
||||
File: "stdout",
|
||||
},
|
||||
JWT: JWT{
|
||||
SecretKey: "",
|
||||
RefreshTokenDuration: 2678400,
|
||||
RevokeTokensOnLogout: true,
|
||||
},
|
||||
Stun: "stun.l.google.com:19302",
|
||||
Turn: Turn{
|
||||
TurnAddr: "turn.cloudflare.com:3478",
|
||||
TurnUser: "",
|
||||
TurnCred: "",
|
||||
},
|
||||
Authentication: "enable",
|
||||
}
|
||||
|
||||
func checkDefaultValue() {
|
||||
if instance.JWT.SecretKey == "" {
|
||||
instance.JWT.SecretKey = generateRandomSecretKey()
|
||||
instance.JWT.RevokeTokensOnLogout = true
|
||||
}
|
||||
|
||||
if instance.JWT.RefreshTokenDuration == 0 {
|
||||
instance.JWT.RefreshTokenDuration = 2678400
|
||||
}
|
||||
|
||||
if instance.Stun == "" {
|
||||
instance.Stun = "stun.l.google.com:19302"
|
||||
}
|
||||
|
||||
if instance.Authentication == "" {
|
||||
instance.Authentication = "enable"
|
||||
}
|
||||
|
||||
instance.Hardware = getHardware()
|
||||
}
|
||||
|
||||
@@ -12,19 +12,34 @@ type HWVersion int
|
||||
const (
|
||||
HWVersionAlpha HWVersion = iota
|
||||
HWVersionBeta
|
||||
HWVersionPcie
|
||||
|
||||
HWVersionFile = "/etc/kvm/hw"
|
||||
)
|
||||
|
||||
const (
|
||||
hwVersionFile = "/etc/kvm/hw"
|
||||
var HWAlpha = Hardware{
|
||||
Version: HWVersionAlpha,
|
||||
GPIOReset: "/sys/class/gpio/gpio507/value",
|
||||
GPIOPower: "/sys/class/gpio/gpio503/value",
|
||||
GPIOPowerLED: "/sys/class/gpio/gpio504/value",
|
||||
GPIOHDDLed: "/sys/class/gpio/gpio505/value",
|
||||
}
|
||||
|
||||
gpioPower = "/sys/class/gpio/gpio503/value"
|
||||
gpioPowerLED = "/sys/class/gpio/gpio504/value"
|
||||
var HWBeta = Hardware{
|
||||
Version: HWVersionBeta,
|
||||
GPIOReset: "/sys/class/gpio/gpio505/value",
|
||||
GPIOPower: "/sys/class/gpio/gpio503/value",
|
||||
GPIOPowerLED: "/sys/class/gpio/gpio504/value",
|
||||
GPIOHDDLed: "",
|
||||
}
|
||||
|
||||
gpioResetAlpha = "/sys/class/gpio/gpio507/value"
|
||||
gpioHDDLedAlpha = "/sys/class/gpio/gpio505/value"
|
||||
|
||||
gpioResetBeta = "/sys/class/gpio/gpio505/value"
|
||||
)
|
||||
var HWPcie = Hardware{
|
||||
Version: HWVersionPcie,
|
||||
GPIOReset: "/sys/class/gpio/gpio505/value",
|
||||
GPIOPower: "/sys/class/gpio/gpio503/value",
|
||||
GPIOPowerLED: "/sys/class/gpio/gpio504/value",
|
||||
GPIOHDDLed: "",
|
||||
}
|
||||
|
||||
func (h HWVersion) String() string {
|
||||
switch h {
|
||||
@@ -32,39 +47,46 @@ func (h HWVersion) String() string {
|
||||
return "Alpha"
|
||||
case HWVersionBeta:
|
||||
return "Beta"
|
||||
case HWVersionPcie:
|
||||
return "PCIE"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func getHwVersion() HWVersion {
|
||||
content, err := os.ReadFile(hwVersionFile)
|
||||
if err == nil {
|
||||
version := strings.ReplaceAll(string(content), "\n", "")
|
||||
if version == "beta" {
|
||||
return HWVersionBeta
|
||||
}
|
||||
content, err := os.ReadFile(HWVersionFile)
|
||||
if err != nil {
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
version := strings.ReplaceAll(string(content), "\n", "")
|
||||
if version == "beta" {
|
||||
return HWVersionBeta
|
||||
} else if version == "pcie" {
|
||||
return HWVersionPcie
|
||||
}
|
||||
|
||||
return HWVersionAlpha
|
||||
}
|
||||
|
||||
func getHardware() Hardware {
|
||||
h := Hardware{}
|
||||
func getHardware() (h Hardware) {
|
||||
version := getHwVersion()
|
||||
|
||||
h.Version = getHwVersion()
|
||||
h.GPIOPower = gpioPower
|
||||
h.GPIOPowerLED = gpioPowerLED
|
||||
|
||||
switch h.Version {
|
||||
switch version {
|
||||
case HWVersionAlpha:
|
||||
h.GPIOHDDLed = gpioHDDLedAlpha
|
||||
h.GPIOReset = gpioResetAlpha
|
||||
h = HWAlpha
|
||||
|
||||
case HWVersionBeta:
|
||||
h.GPIOReset = gpioResetBeta
|
||||
h = HWBeta
|
||||
|
||||
case HWVersionPcie:
|
||||
h = HWPcie
|
||||
|
||||
default:
|
||||
log.Fatalf("Unsupported hardware version: %s", h.Version)
|
||||
h = HWAlpha
|
||||
log.Error("Unsupported hardware version: %s", version)
|
||||
}
|
||||
|
||||
return h
|
||||
return
|
||||
}
|
||||
|
||||
28
server/config/jwt.go
Normal file
28
server/config/jwt.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RegenerateSecretKey regenerate secret key when logout
|
||||
func RegenerateSecretKey() {
|
||||
if instance.JWT.RevokeTokensOnLogout {
|
||||
instance.JWT.SecretKey = generateRandomSecretKey()
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random string for secret key.
|
||||
func generateRandomSecretKey() string {
|
||||
b := make([]byte, 64)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
currentTime := time.Now().UnixNano()
|
||||
timeString := fmt.Sprintf("%d", currentTime)
|
||||
return fmt.Sprintf("%064s", timeString)
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Protocol string `yaml:"proto"`
|
||||
Proto string `yaml:"proto"`
|
||||
Port Port `yaml:"port"`
|
||||
Cert Cert `yaml:"cert"`
|
||||
Logger Logger `yaml:"logger"`
|
||||
Authentication string `yaml:"authentication"`
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
JWT JWT `yaml:"jwt"`
|
||||
Stun string `yaml:"stun"`
|
||||
Turn Turn `yaml:"turn"`
|
||||
|
||||
Hardware Hardware `yaml:"-"`
|
||||
}
|
||||
@@ -26,6 +28,18 @@ type Cert struct {
|
||||
Key string `yaml:"key"`
|
||||
}
|
||||
|
||||
type JWT struct {
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
RefreshTokenDuration uint64 `yaml:"refreshTokenDuration"`
|
||||
RevokeTokensOnLogout bool `yaml:"revokeTokensOnLogout"`
|
||||
}
|
||||
|
||||
type Turn struct {
|
||||
TurnAddr string `yaml:"turnAddr"`
|
||||
TurnUser string `yaml:"turnUser"`
|
||||
TurnCred string `yaml:"turnCred"`
|
||||
}
|
||||
|
||||
type Hardware struct {
|
||||
Version HWVersion `yaml:"-"`
|
||||
GPIOReset string `yaml:"-"`
|
||||
|
||||
BIN
server/dl_lib/libaaccomm2.so
Normal file
BIN
server/dl_lib/libaaccomm2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libaacdec2.so
Normal file
BIN
server/dl_lib/libaacdec2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libaacenc2.so
Normal file
BIN
server/dl_lib/libaacenc2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libaacsbrdec2.so
Normal file
BIN
server/dl_lib/libaacsbrdec2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libaacsbrenc2.so
Normal file
BIN
server/dl_lib/libaacsbrenc2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libae.so
Normal file
BIN
server/dl_lib/libae.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libaf.so
Normal file
BIN
server/dl_lib/libaf.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libawb.so
Normal file
BIN
server/dl_lib/libawb.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcli.so
Normal file
BIN
server/dl_lib/libcli.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_RES1.so
Normal file
BIN
server/dl_lib/libcvi_RES1.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_VoiceEngine.so
Normal file
BIN
server/dl_lib/libcvi_VoiceEngine.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_audio.so
Normal file
BIN
server/dl_lib/libcvi_audio.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_bin.so
Normal file
BIN
server/dl_lib/libcvi_bin.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_bin_isp.so
Normal file
BIN
server/dl_lib/libcvi_bin_isp.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_ispd2.so
Normal file
BIN
server/dl_lib/libcvi_ispd2.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_ive.so
Normal file
BIN
server/dl_lib/libcvi_ive.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_ssp.so
Normal file
BIN
server/dl_lib/libcvi_ssp.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libcvi_vqe.so
Normal file
BIN
server/dl_lib/libcvi_vqe.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libdnvqe.so
Normal file
BIN
server/dl_lib/libdnvqe.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libini.so
Normal file
BIN
server/dl_lib/libini.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libisp.so
Normal file
BIN
server/dl_lib/libisp.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libisp_algo.so
Normal file
BIN
server/dl_lib/libisp_algo.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libjson-c.so.5
Normal file
BIN
server/dl_lib/libjson-c.so.5
Normal file
Binary file not shown.
Binary file not shown.
BIN
server/dl_lib/libkvm_mmf.so
Normal file
BIN
server/dl_lib/libkvm_mmf.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libmipi_tx.so
Normal file
BIN
server/dl_lib/libmipi_tx.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libmisc.so
Normal file
BIN
server/dl_lib/libmisc.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libopencv_core.so.409
Normal file
BIN
server/dl_lib/libopencv_core.so.409
Normal file
Binary file not shown.
BIN
server/dl_lib/libopencv_highgui.so.409
Normal file
BIN
server/dl_lib/libopencv_highgui.so.409
Normal file
Binary file not shown.
BIN
server/dl_lib/libopencv_imgcodecs.so.409
Normal file
BIN
server/dl_lib/libopencv_imgcodecs.so.409
Normal file
Binary file not shown.
BIN
server/dl_lib/libopencv_imgproc.so.409
Normal file
BIN
server/dl_lib/libopencv_imgproc.so.409
Normal file
Binary file not shown.
BIN
server/dl_lib/libosdc.so
Normal file
BIN
server/dl_lib/libosdc.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libraw_dump.so
Normal file
BIN
server/dl_lib/libraw_dump.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libsys.so
Normal file
BIN
server/dl_lib/libsys.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libtinyalsa.so
Normal file
BIN
server/dl_lib/libtinyalsa.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libvdec.so
Normal file
BIN
server/dl_lib/libvdec.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libvenc.so
Normal file
BIN
server/dl_lib/libvenc.so
Normal file
Binary file not shown.
BIN
server/dl_lib/libvpu.so
Normal file
BIN
server/dl_lib/libvpu.so
Normal file
Binary file not shown.
@@ -3,6 +3,7 @@ module NanoKVM-Server
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
@@ -14,7 +15,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/unrolled/secure v1.15.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -70,9 +71,9 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -156,21 +158,19 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
#ifndef KVM_VISION_H_
|
||||
#define KVM_VISION_H_
|
||||
|
||||
@@ -35,6 +37,7 @@ void kvmv_init(uint8_t _debug_info_en);
|
||||
* @param _pp_kvm_data @output: Encode data
|
||||
* @param _p_kvmv_data_size @output: Encode data size
|
||||
* @return
|
||||
-4: Modifying image resolution, please wait
|
||||
-3: img buffer full
|
||||
-2: VENC Error
|
||||
-1: No images were acquired
|
||||
@@ -51,6 +54,7 @@ int free_kvmv_data(uint8_t ** _pp_kvm_data);
|
||||
void free_all_kvmv_data();
|
||||
void set_h264_gop(uint8_t _gop);
|
||||
void kvmv_deinit();
|
||||
uint8_t kvmv_hdmi_control(uint8_t _en);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ import (
|
||||
"NanoKVM-Server/logger"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/router"
|
||||
"NanoKVM-Server/utils"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
@@ -18,31 +21,53 @@ import (
|
||||
func main() {
|
||||
initialize()
|
||||
defer dispose()
|
||||
signalHandler()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(cors.AllowAll())
|
||||
|
||||
router.Init(r)
|
||||
|
||||
run(r)
|
||||
run()
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
logger.Init()
|
||||
|
||||
// init screen parameters
|
||||
_ = common.GetScreen()
|
||||
_ = common.GetKvmVision()
|
||||
|
||||
// init HDMI
|
||||
vision := common.GetKvmVision()
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
vision.SetHDMI(true)
|
||||
|
||||
utils.InitGoMemLimit()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
log.Printf("\nReceived signal: %v\n", sig)
|
||||
|
||||
dispose()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func run(r *gin.Engine) {
|
||||
func run() {
|
||||
conf := config.GetInstance()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
if conf.Authentication == "disable" {
|
||||
r.Use(cors.AllowAll())
|
||||
}
|
||||
|
||||
router.Init(r)
|
||||
|
||||
httpAddr := fmt.Sprintf(":%d", conf.Port.Http)
|
||||
httpsAddr := fmt.Sprintf(":%d", conf.Port.Https)
|
||||
log.Printf("proto: %s, port: %d %d\n", conf.Proto, conf.Port.Http, conf.Port.Https)
|
||||
|
||||
if conf.Protocol == "https" {
|
||||
if conf.Proto == "https" {
|
||||
r.Use(middleware.Tls())
|
||||
|
||||
go func() {
|
||||
@@ -64,16 +89,3 @@ func run(r *gin.Engine) {
|
||||
func dispose() {
|
||||
common.GetKvmVision().Close()
|
||||
}
|
||||
|
||||
func signalHandler() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
fmt.Printf("\nReceived signal: %v\n", sig)
|
||||
|
||||
dispose()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ type Token struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
const (
|
||||
ExpireDuration = 31 * 24 * time.Hour
|
||||
)
|
||||
|
||||
func CheckToken() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
conf := config.GetInstance()
|
||||
@@ -46,24 +42,25 @@ func CheckToken() gin.HandlerFunc {
|
||||
func GenerateJWT(username string) (string, error) {
|
||||
conf := config.GetInstance()
|
||||
|
||||
expireDuration := time.Duration(conf.JWT.RefreshTokenDuration) * time.Second
|
||||
|
||||
claims := Token{
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ExpireDuration)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return t.SignedString([]byte(conf.SecretKey))
|
||||
return t.SignedString([]byte(conf.JWT.SecretKey))
|
||||
}
|
||||
|
||||
func ParseJWT(jwtToken string) (*Token, error) {
|
||||
conf := config.GetInstance()
|
||||
|
||||
t, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) {
|
||||
secretKey := conf.SecretKey
|
||||
return []byte(secretKey), nil
|
||||
return []byte(conf.JWT.SecretKey), nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Debugf("parse jwt error: %s", err)
|
||||
|
||||
@@ -4,7 +4,3 @@ type GetVersionRsp struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
}
|
||||
|
||||
type GetLibRsp struct {
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ type LoginRsp struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type GetAccountRsp struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type ChangePasswordReq struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
@@ -17,3 +21,8 @@ type ChangePasswordReq struct {
|
||||
type IsPasswordUpdatedRsp struct {
|
||||
IsUpdated bool `json:"isUpdated"`
|
||||
}
|
||||
|
||||
type ConnectWifiReq struct {
|
||||
Ssid string `validate:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
11
server/proto/download.go
Normal file
11
server/proto/download.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package proto
|
||||
|
||||
type ImageEnabledRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type StatusImageRsp struct {
|
||||
Status string `json:"status"`
|
||||
File string `json:"file"`
|
||||
Percentage string `json:"percentage"`
|
||||
}
|
||||
@@ -12,22 +12,28 @@ type DeleteMacReq struct {
|
||||
Mac string `form:"mac" validate:"required"`
|
||||
}
|
||||
|
||||
type TailscaleState string
|
||||
|
||||
const (
|
||||
TailscaleNotInstall TailscaleState = "notInstall"
|
||||
TailscaleNotRunning TailscaleState = "notRunning"
|
||||
TailscaleNotLogin TailscaleState = "notLogin"
|
||||
TailscaleStopped TailscaleState = "stopped"
|
||||
TailscaleRunning TailscaleState = "running"
|
||||
)
|
||||
|
||||
type GetTailscaleStatusRsp struct {
|
||||
Status string `json:"status"` // notInstall | notLogin | stopped | running
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
type UpdateTailscaleStatusReq struct {
|
||||
Command string // up | down
|
||||
}
|
||||
|
||||
type UpdateTailscaleStatusRsp struct {
|
||||
Status string `json:"status"` // stopped | running
|
||||
State TailscaleState `json:"state"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
type LoginTailscaleRsp struct {
|
||||
Status string `json:"status"`
|
||||
Url string `json:"url"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type GetWifiRsp struct {
|
||||
Supported bool `json:"supported"`
|
||||
Connected bool `json:"connected"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ type GetInfoRsp struct {
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type GetHardwareRsp struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type SetGpioReq struct {
|
||||
Type string `validate:"required"` // reset / power
|
||||
Duration uint `validate:"omitempty"` // press time (unit: milliseconds)
|
||||
@@ -56,3 +60,26 @@ type UpdateVirtualDeviceReq struct {
|
||||
type UpdateVirtualDeviceRsp struct {
|
||||
On bool `json:"on"`
|
||||
}
|
||||
|
||||
type SetMemoryLimitReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetMemoryLimitRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type SetOledReq struct {
|
||||
Sleep int `json:"sleep"`
|
||||
}
|
||||
|
||||
type GetOLEDRsp struct {
|
||||
Exist bool `json:"exist"`
|
||||
Sleep int `json:"sleep"`
|
||||
}
|
||||
|
||||
type GetSSHStateRsp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
@@ -15,5 +15,7 @@ func authRouter(r *gin.Engine) {
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/auth/password", service.IsPasswordUpdated) // is password updated
|
||||
api.GET("/auth/account", service.GetAccount) // get account
|
||||
api.POST("/auth/password", service.ChangePassword) // change password
|
||||
api.POST("/auth/logout", service.Logout) // logout
|
||||
}
|
||||
|
||||
17
server/router/download.go
Normal file
17
server/router/download.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/service/download"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"NanoKVM-Server/middleware"
|
||||
)
|
||||
|
||||
func downloadRouter(r *gin.Engine) {
|
||||
service := download.NewService()
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.POST("/download/image", service.DownloadImage) // download image
|
||||
api.GET("/download/image/status", service.StatusImage) // download image
|
||||
api.GET("/download/image/enabled", service.ImageEnabled) // download image
|
||||
}
|
||||
25
server/router/extensions.go
Normal file
25
server/router/extensions.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/service/extensions/tailscale"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func extensionsRouter(r *gin.Engine) {
|
||||
api := r.Group("/api/extensions").Use(middleware.CheckToken())
|
||||
|
||||
ts := tailscale.NewService()
|
||||
|
||||
api.POST("/tailscale/install", ts.Install) // install tailscale
|
||||
api.POST("/tailscale/uninstall", ts.Uninstall) // uninstall tailscale
|
||||
api.GET("/tailscale/status", ts.GetStatus) // get tailscale status
|
||||
api.POST("/tailscale/up", ts.Up) // run tailscale up
|
||||
api.POST("/tailscale/down", ts.Down) // run tailscale down
|
||||
api.POST("/tailscale/login", ts.Login) // tailscale login
|
||||
api.POST("/tailscale/logout", ts.Logout) // tailscale logout
|
||||
api.POST("/tailscale/start", ts.Start) // tailscale start
|
||||
api.POST("/tailscale/stop", ts.Stop) // tailscale stop
|
||||
api.POST("/tailscale/restart", ts.Restart) // tailscale restart
|
||||
}
|
||||
@@ -9,15 +9,14 @@ import (
|
||||
|
||||
func networkRouter(r *gin.Engine) {
|
||||
service := network.NewService()
|
||||
|
||||
r.POST("/api/network/wifi", service.ConnectWifi) // connect Wi-Fi
|
||||
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.POST("/network/wol", service.WakeOnLAN) // wake on lan
|
||||
api.GET("/network/wol/mac", service.GetMac) // get mac list
|
||||
api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac
|
||||
|
||||
api.POST("/network/tailscale/install", service.InstallTailscale) // install tailscale
|
||||
api.GET("/network/tailscale/status", service.GetTailscaleStatus) // get tailscale status
|
||||
api.POST("/network/tailscale/status", service.UpdateTailscaleStatus) // update tailscale status
|
||||
api.POST("/network/tailscale/login", service.LoginTailscale) // tailscale login
|
||||
api.POST("/network/tailscale/logout", service.LogoutTailscale) // tailscale logout
|
||||
api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information
|
||||
}
|
||||
|
||||
@@ -37,4 +37,6 @@ func server(r *gin.Engine) {
|
||||
networkRouter(r)
|
||||
hidRouter(r)
|
||||
wsRouter(r)
|
||||
downloadRouter(r)
|
||||
extensionsRouter(r)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ func vmRouter(r *gin.Engine) {
|
||||
service := vm.NewService()
|
||||
api := r.Group("/api").Use(middleware.CheckToken())
|
||||
|
||||
api.GET("/vm/info", service.GetInfo) // get device information
|
||||
api.GET("/vm/info", service.GetInfo) // get device information
|
||||
api.GET("/vm/hardware", service.GetHardware) // get hardware version
|
||||
|
||||
api.POST("/vm/gpio", service.SetGpio) // update gpio
|
||||
api.GET("/vm/gpio", service.GetGpio) // get gpio
|
||||
@@ -26,4 +27,16 @@ func vmRouter(r *gin.Engine) {
|
||||
|
||||
api.GET("/vm/device/virtual", service.GetVirtualDevice) // get virtual device
|
||||
api.POST("/vm/device/virtual", service.UpdateVirtualDevice) // update virtual device
|
||||
|
||||
api.GET("/vm/memory/limit", service.GetMemoryLimit) // get memory limit
|
||||
api.POST("/vm/memory/limit", service.SetMemoryLimit) // set memory limit
|
||||
|
||||
api.GET("/vm/oled", service.GetOLED) // get OLED configuration
|
||||
api.POST("/vm/oled", service.SetOLED) // set OLED configuration
|
||||
|
||||
api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi (pcie only)
|
||||
|
||||
api.GET("/vm/ssh", service.GetSSHState) // get SSH state
|
||||
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
|
||||
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
maxTries = 3
|
||||
)
|
||||
|
||||
func (s *Service) GetVersion(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
log.Debugf("get version api triggered")
|
||||
|
||||
// current version
|
||||
currentVersion := "1.0.0"
|
||||
content, err := os.ReadFile(versionFile)
|
||||
if err == nil {
|
||||
currentVersion = strings.ReplaceAll(string(content), "\n", "")
|
||||
}
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
url := fmt.Sprintf("%s?now=%d", versionURL, time.Now().Unix())
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("get latest version failed: %v", err)
|
||||
rsp.ErrRsp(c, -2, "Unable to access sipeed.com. Please check your network.")
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("read body failed: %v", err)
|
||||
rsp.ErrRsp(c, -4, "read body failed")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
rsp.ErrRsp(c, -3, fmt.Sprintf("get version failed, server returns status code: %d, body: %s", resp.StatusCode, string(body)))
|
||||
return
|
||||
}
|
||||
|
||||
latestVersion := strings.ReplaceAll(string(body), "\n", "")
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latestVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Update(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
log.Debugf("update application api triggered")
|
||||
|
||||
if err := updateApp(); err != nil {
|
||||
log.Debugf("update failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("update application success")
|
||||
|
||||
// Sleep for a second before restarting the device
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
|
||||
}
|
||||
|
||||
func updateApp() error {
|
||||
log.Debugf("update application")
|
||||
_ = os.RemoveAll(temporary)
|
||||
_ = os.MkdirAll(temporary, 0o755)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(temporary)
|
||||
}()
|
||||
|
||||
if err := downloadLib(); err != nil {
|
||||
log.Errorf("download lib failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := downloadApp(); err != nil {
|
||||
log.Errorf("download app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err := utils.MoveFile(temporary+"/"+libName, temporary+"/latest/kvm_system/dl_lib/"+libName) // move lib
|
||||
if err != nil {
|
||||
log.Errorf("rename lib failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// backup old version
|
||||
err = os.RemoveAll(backup)
|
||||
if err != nil {
|
||||
log.Errorf("remove backup failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = utils.MoveFilesRecursively(workspace, backup)
|
||||
if err != nil {
|
||||
log.Errorf("backuping old libraries failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
err = utils.MoveFilesRecursively(temporary+"/latest", workspace)
|
||||
if err != nil {
|
||||
log.Errorf("failed to move update back in place: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// modify permissions
|
||||
err = utils.ChmodRecursively(workspace, 0o755)
|
||||
if err != nil {
|
||||
log.Errorf("chmod failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadApp() error {
|
||||
var err error
|
||||
log.Debugf("downloading latest application...")
|
||||
url := fmt.Sprintf("%s?now=%d", applicationURL, time.Now().Unix())
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
if i > 0 {
|
||||
time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Errorf("new request err: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
zipFile := temporary + "/latest.zip"
|
||||
log.Debugf("update will be saved to: %s", zipFile)
|
||||
err = utils.Download(req, zipFile)
|
||||
if err != nil {
|
||||
log.Errorf("downloading latest application failed, try again...")
|
||||
continue
|
||||
}
|
||||
|
||||
err = utils.Unzip(zipFile, temporary)
|
||||
if err != nil {
|
||||
log.Errorf("unzip app failed: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func downloadLib() error {
|
||||
log.Debugf("downloading libs...")
|
||||
content, err := os.ReadFile("/device_key")
|
||||
if err != nil {
|
||||
log.Errorf("error reading device key: %s", err)
|
||||
return err
|
||||
}
|
||||
deviceKey := strings.ReplaceAll(string(content), "\n", "")
|
||||
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
if i > 0 {
|
||||
time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
url := fmt.Sprintf("%s?uid=%s", libURL, deviceKey)
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Errorf("error creating new request: %s", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("token", "MaixVision2024")
|
||||
|
||||
target := fmt.Sprintf("%s/%s", temporary, libName)
|
||||
|
||||
err = utils.Download(req, target)
|
||||
if err != nil {
|
||||
log.Errorf("downloading lib failed: %s", err)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
versionURL = "https://cdn.sipeed.com/nanokvm/latest"
|
||||
applicationURL = "https://cdn.sipeed.com/nanokvm/latest.zip"
|
||||
libURL = "https://maixvision.sipeed.com/api/v1/nanokvm/encryption"
|
||||
BaseURL = "https://cdn.sipeed.com/nanokvm"
|
||||
|
||||
temporary = "/root/.kvmcache"
|
||||
workspace = "/kvmapp"
|
||||
backup = "/root/old"
|
||||
libName = "libmaixcam_lib.so"
|
||||
versionFile = "/kvmapp/version"
|
||||
AppDir = "/kvmapp"
|
||||
BackupDir = "/root/old"
|
||||
CacheDir = "/root/.kvmcache"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
222
server/service/application/update.go
Normal file
222
server/service/application/update.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
maxTries = 3
|
||||
)
|
||||
|
||||
type Latest struct {
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Sha512 string `json:"sha512"`
|
||||
Size uint `json:"size"`
|
||||
}
|
||||
|
||||
func (s *Service) GetVersion(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// current version
|
||||
currentVersion := "1.0.0"
|
||||
|
||||
versionFile := fmt.Sprintf("%s/version", AppDir)
|
||||
if version, err := os.ReadFile(versionFile); err == nil {
|
||||
currentVersion = strings.ReplaceAll(string(version), "\n", "")
|
||||
}
|
||||
|
||||
log.Debugf("current version: %s", currentVersion)
|
||||
|
||||
// latest version
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get latest version failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetVersionRsp{
|
||||
Current: currentVersion,
|
||||
Latest: latest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Update(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if err := update(); err != nil {
|
||||
rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("update application success")
|
||||
|
||||
// Sleep for a second before restarting the device
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
|
||||
}
|
||||
|
||||
func update() error {
|
||||
_ = os.RemoveAll(CacheDir)
|
||||
_ = os.MkdirAll(CacheDir, 0o755)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(CacheDir)
|
||||
}()
|
||||
|
||||
// get latest information
|
||||
latest, err := getLatest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// download
|
||||
url := fmt.Sprintf("%s/%s", BaseURL, latest.Name)
|
||||
target := fmt.Sprintf("%s/%s", CacheDir, latest.Name)
|
||||
|
||||
if err := download(url, target); err != nil {
|
||||
log.Errorf("download app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// check sha512
|
||||
if err := checksum(target, latest.Sha512); err != nil {
|
||||
log.Errorf("check sha512 failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// decompress
|
||||
dir, err := utils.UnTarGz(target, CacheDir)
|
||||
log.Debugf("untar: %s", dir)
|
||||
if err != nil {
|
||||
log.Errorf("decompress app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// backup old version
|
||||
if err := os.RemoveAll(BackupDir); err != nil {
|
||||
log.Errorf("remove backup failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil {
|
||||
log.Errorf("backup app failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
if err := utils.MoveFilesRecursively(dir, AppDir); err != nil {
|
||||
log.Errorf("failed to move update back in place: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// modify permissions
|
||||
if err := utils.ChmodRecursively(AppDir, 0o755); err != nil {
|
||||
log.Errorf("chmod failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLatest() (*Latest, error) {
|
||||
url := fmt.Sprintf("%s/latest.json?now=%d", BaseURL, time.Now().Unix())
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("server responded with status code: %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var latest Latest
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
log.Errorf("failed to unmarshal response: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("get application latest version: %s", latest.Version)
|
||||
return &latest, nil
|
||||
}
|
||||
|
||||
func download(url string, target string) (err error) {
|
||||
for i := range maxTries {
|
||||
log.Debugf("attempt #%d/%d", i+1, maxTries)
|
||||
if i > 0 {
|
||||
time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Errorf("new request err: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("update will be saved to: %s", target)
|
||||
err = utils.Download(req, target)
|
||||
if err != nil {
|
||||
log.Errorf("downloading latest application failed, try again...")
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func checksum(filePath string, expectedHash string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open file %s: %v", filePath, err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
hasher := sha512.New()
|
||||
|
||||
_, err = io.Copy(hasher, file)
|
||||
if err != nil {
|
||||
log.Errorf("failed to copy file contents to hasher: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
hash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
if hash != expectedHash {
|
||||
log.Errorf("invalid sha512 %s", hash)
|
||||
return fmt.Errorf("invalid sha512 %s", hash)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/utils"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const AccountFile = "/etc/kvm/pwd"
|
||||
@@ -18,16 +18,11 @@ type Account struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func getAccount() (*Account, error) {
|
||||
// use default account
|
||||
func GetAccount() (*Account, error) {
|
||||
if _, err := os.Stat(AccountFile); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &Account{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
}, nil
|
||||
return getDefaultAccount(), nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,23 +32,15 @@ func getAccount() (*Account, error) {
|
||||
}
|
||||
|
||||
var account Account
|
||||
err = json.Unmarshal(content, &account)
|
||||
if err != nil {
|
||||
if err = json.Unmarshal(content, &account); err != nil {
|
||||
log.Errorf("unmarshal account failed: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password, err := utils.DecodeDecrypt(account.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account.Password = password
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func setAccount(username string, password string) error {
|
||||
func SetAccount(username string, password string) error {
|
||||
account, err := json.Marshal(&Account{
|
||||
Username: username,
|
||||
Password: password,
|
||||
@@ -77,3 +64,50 @@ func setAccount(username string, password string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CompareAccount(username string, password string) bool {
|
||||
account, err := GetAccount()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if username != account.Username {
|
||||
return false
|
||||
}
|
||||
|
||||
decryptedPassword, err := utils.DecodeDecrypt(password)
|
||||
if err != nil || decryptedPassword == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(decryptedPassword))
|
||||
if err != nil {
|
||||
// Compatible with old versions
|
||||
accountDecryptedPassword, _ := utils.DecodeDecrypt(account.Password)
|
||||
if accountDecryptedPassword == decryptedPassword {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func DelAccount() error {
|
||||
if err := os.Remove(AccountFile); err != nil {
|
||||
log.Errorf("failed to delete password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDefaultAccount() *Account {
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
|
||||
return &Account{
|
||||
Username: "admin",
|
||||
Password: string(password),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
func (s *Service) Login(c *gin.Context) {
|
||||
var req proto.LoginReq
|
||||
var rsp proto.Response
|
||||
|
||||
// authentication disabled
|
||||
conf := config.GetInstance()
|
||||
if conf.Authentication == "disable" {
|
||||
rsp.OkRspWithData(c, &proto.LoginRsp{
|
||||
Token: "disabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
passwordDecrypt, err := utils.DecodeDecrypt(req.Password)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "decrypt password failed")
|
||||
return
|
||||
}
|
||||
|
||||
account, err := getAccount()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -3, "get account failed")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username != account.Username || passwordDecrypt != account.Password {
|
||||
rsp.ErrRsp(c, -4, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateJWT(req.Username)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -5, "generate token failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.LoginRsp{
|
||||
Token: token,
|
||||
})
|
||||
|
||||
log.Debugf("login success, username: %s", req.Username)
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(c *gin.Context) {
|
||||
var req proto.ChangePasswordReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if err := setAccount(req.Username, req.Password); err != nil {
|
||||
rsp.ErrRsp(c, -2, "change password failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("change password success, username: %s", req.Username)
|
||||
}
|
||||
|
||||
func (s *Service) IsPasswordUpdated(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
account, err := getAccount()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "failed to get password")
|
||||
}
|
||||
|
||||
isUpdated := true
|
||||
if account == nil || account.Password == "admin" {
|
||||
isUpdated = false
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{
|
||||
IsUpdated: isUpdated,
|
||||
})
|
||||
log.Debugf("get password success")
|
||||
}
|
||||
72
server/service/auth/login.go
Normal file
72
server/service/auth/login.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"NanoKVM-Server/middleware"
|
||||
"NanoKVM-Server/proto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) Login(c *gin.Context) {
|
||||
var req proto.LoginReq
|
||||
var rsp proto.Response
|
||||
|
||||
// authentication disabled
|
||||
conf := config.GetInstance()
|
||||
if conf.Authentication == "disable" {
|
||||
rsp.OkRspWithData(c, &proto.LoginRsp{
|
||||
Token: "disabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if ok := CompareAccount(req.Username, req.Password); !ok {
|
||||
rsp.ErrRsp(c, -2, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateJWT(req.Username)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -3, "generate token failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.LoginRsp{
|
||||
Token: token,
|
||||
})
|
||||
|
||||
log.Debugf("login success, username: %s", req.Username)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(c *gin.Context) {
|
||||
conf := config.GetInstance()
|
||||
|
||||
if conf.JWT.RevokeTokensOnLogout {
|
||||
config.RegenerateSecretKey()
|
||||
}
|
||||
|
||||
var rsp proto.Response
|
||||
rsp.OkRsp(c)
|
||||
}
|
||||
|
||||
func (s *Service) GetAccount(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
account, err := GetAccount()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "get account failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetAccountRsp{
|
||||
Username: account.Username,
|
||||
})
|
||||
log.Debugf("get account successful")
|
||||
}
|
||||
121
server/service/auth/password.go
Normal file
121
server/service/auth/password.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s *Service) ChangePassword(c *gin.Context) {
|
||||
var req proto.ChangePasswordReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
password, err := utils.DecodeDecrypt(req.Password)
|
||||
if err != nil || password == "" {
|
||||
rsp.ErrRsp(c, -2, "invalid password")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -3, "failed to hash password")
|
||||
return
|
||||
}
|
||||
|
||||
if err = SetAccount(req.Username, string(hashedPassword)); err != nil {
|
||||
rsp.ErrRsp(c, -4, "failed to save password")
|
||||
return
|
||||
}
|
||||
|
||||
// change root password
|
||||
err = changeRootPassword(password)
|
||||
if err != nil {
|
||||
_ = DelAccount()
|
||||
rsp.ErrRsp(c, -5, "failed to change password")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("change password success, username: %s", req.Username)
|
||||
}
|
||||
|
||||
func (s *Service) IsPasswordUpdated(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if _, err := os.Stat(AccountFile); err != nil {
|
||||
rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{
|
||||
IsUpdated: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
account, err := GetAccount()
|
||||
if err != nil || account == nil {
|
||||
rsp.ErrRsp(c, -1, "failed to get password")
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte("admin"))
|
||||
|
||||
rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{
|
||||
IsUpdated: err == nil,
|
||||
})
|
||||
}
|
||||
|
||||
func changeRootPassword(password string) error {
|
||||
err := passwd(password)
|
||||
if err != nil {
|
||||
log.Errorf("failed to change root password: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("change root password successful.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func passwd(password string) error {
|
||||
cmd := exec.Command("passwd", "root")
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = stdin.Close()
|
||||
}()
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.WriteString(stdin, password+"\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if _, err = io.WriteString(stdin, password+"\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
230
server/service/download/service.go
Normal file
230
server/service/download/service.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
var sentinelPath = "/tmp/.download_in_progress"
|
||||
|
||||
func NewService() *Service {
|
||||
// Clear sentinel
|
||||
// If we are starting from scratch, we need to remove the sentinel file as any downloads at this point are done or broken
|
||||
_ = os.Remove(sentinelPath)
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) ImageEnabled(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// Check if /data mount is RO/RW
|
||||
testFile := "/data/.testfile"
|
||||
file, err := os.Create(testFile)
|
||||
defer file.Close()
|
||||
defer os.Remove(testFile)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
||||
Enabled: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
||||
Enabled: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) StatusImage(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// Check if the sentinel file exists
|
||||
log.Debug("StatusImage")
|
||||
if _, err := os.Stat(sentinelPath); err == nil {
|
||||
content, err := os.ReadFile(sentinelPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to read sentinel file")
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "in_progress",
|
||||
File: "",
|
||||
Percentage: "",
|
||||
})
|
||||
return
|
||||
}
|
||||
splitted := strings.Split(string(content), ";")
|
||||
if len(splitted) == 1 {
|
||||
// No percentage, just the URL
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "in_progress",
|
||||
File: splitted[0],
|
||||
Percentage: "",
|
||||
})
|
||||
} else {
|
||||
// Percentage is available
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "in_progress",
|
||||
File: splitted[0],
|
||||
Percentage: splitted[1],
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "idle",
|
||||
File: "",
|
||||
Percentage: "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImage(c *gin.Context) {
|
||||
var req proto.MountImageReq
|
||||
var rsp proto.Response
|
||||
|
||||
log.Debug("DownloadImage")
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if req.File == "" {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
// Parse the URI to see if its valid http/s
|
||||
u, err := url.Parse(req.File)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
rsp.ErrRsp(c, -1, "invalid url")
|
||||
return
|
||||
}
|
||||
|
||||
// Set a sentinel file to mark that there is a download in progress
|
||||
// This is to prevent multiple downloads at the same time
|
||||
if _, err := os.Stat(sentinelPath); err == nil {
|
||||
log.Debug("Download in progress")
|
||||
rsp.ErrRsp(c, -1, "download in progress")
|
||||
return
|
||||
}
|
||||
// Create the sentinel file
|
||||
err = os.WriteFile(sentinelPath, []byte(req.File), 0644)
|
||||
if err != nil {
|
||||
log.Error("Failed to create sentinel file")
|
||||
rsp.ErrRsp(c, -1, "failed to create sentinel file")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it actually exists and fail if it doesn't
|
||||
resp, err := http.Head(req.File)
|
||||
if resp.StatusCode != http.StatusOK || err != nil {
|
||||
rsp.ErrRsp(c, resp.StatusCode, "failed when checking the url")
|
||||
log.Error("Failed to check the URL")
|
||||
defer os.Remove(sentinelPath)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Download the image in a goroutine to not block the request
|
||||
go func() {
|
||||
defer os.Remove(sentinelPath)
|
||||
resp, err = http.Get(req.File)
|
||||
if err != nil {
|
||||
log.Error("Failed to download the file")
|
||||
rsp.ErrRsp(c, -1, "failed to download the file")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Create the destination file
|
||||
destPath := filepath.Join("/data", filepath.Base(u.Path))
|
||||
out, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to create destination file")
|
||||
rsp.ErrRsp(c, -1, "failed to create destination file")
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
lw := &loggingWriter{writer: out, totalSize: resp.ContentLength}
|
||||
lw.startTicker()
|
||||
_, err = io.Copy(lw, resp.Body)
|
||||
if err != nil {
|
||||
log.Error("Failed to save the file")
|
||||
rsp.ErrRsp(c, -1, "failed to save the file")
|
||||
lw.stopTicker()
|
||||
return
|
||||
}
|
||||
lw.stopTicker()
|
||||
}()
|
||||
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
||||
Status: "in_progress",
|
||||
File: req.File,
|
||||
Percentage: "",
|
||||
})
|
||||
}
|
||||
|
||||
type loggingWriter struct {
|
||||
writer io.Writer
|
||||
total int64
|
||||
totalSize int64
|
||||
ticker *time.Ticker
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (lw *loggingWriter) startTicker() {
|
||||
lw.ticker = time.NewTicker(2500 * time.Millisecond)
|
||||
lw.done = make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-lw.done:
|
||||
return
|
||||
case <-lw.ticker.C:
|
||||
lw.updateSentinel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (lw *loggingWriter) stopTicker() {
|
||||
lw.ticker.Stop()
|
||||
lw.done <- true
|
||||
}
|
||||
|
||||
func (lw *loggingWriter) updateSentinel() {
|
||||
percentage := float64(lw.total) / float64(lw.totalSize) * 100
|
||||
content, err := os.ReadFile(sentinelPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to read sentinel file")
|
||||
return
|
||||
}
|
||||
splitted := strings.Split(string(content), ";")
|
||||
if len(splitted) == 0 {
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(sentinelPath, []byte(fmt.Sprintf("%s;%.2f%%", splitted[0], percentage)), 0644)
|
||||
if err != nil {
|
||||
log.Error("Failed to update sentinel file")
|
||||
}
|
||||
}
|
||||
|
||||
func (lw *loggingWriter) Write(p []byte) (int, error) {
|
||||
n, err := lw.writer.Write(p)
|
||||
lw.total += int64(n)
|
||||
return n, err
|
||||
}
|
||||
149
server/service/extensions/tailscale/cli.go
Normal file
149
server/service/extensions/tailscale/cli.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/utils"
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ScriptPath = "/etc/init.d/S98tailscaled"
|
||||
ScriptBackupPath = "/kvmapp/system/init.d/S98tailscaled"
|
||||
)
|
||||
|
||||
type Cli struct{}
|
||||
|
||||
type TsStatus struct {
|
||||
BackendState string `json:"BackendState"`
|
||||
|
||||
Self struct {
|
||||
HostName string `json:"HostName"`
|
||||
TailscaleIPs []string `json:"TailscaleIPs"`
|
||||
} `json:"Self"`
|
||||
|
||||
CurrentTailnet struct {
|
||||
Name string `json:"Name"`
|
||||
} `json:"CurrentTailnet"`
|
||||
}
|
||||
|
||||
func NewCli() *Cli {
|
||||
return &Cli{}
|
||||
}
|
||||
|
||||
func (c *Cli) Start() error {
|
||||
for _, filePath := range []string{TailscalePath, TailscaledPath} {
|
||||
if err := utils.EnsurePermission(filePath, 0o100); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commands := []string{
|
||||
fmt.Sprintf("cp -f %s %s", ScriptBackupPath, ScriptPath),
|
||||
fmt.Sprintf("%s start", ScriptPath),
|
||||
}
|
||||
|
||||
command := strings.Join(commands, " && ")
|
||||
return exec.Command("sh", "-c", command).Run()
|
||||
}
|
||||
|
||||
func (c *Cli) Restart() error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("cp -f %s %s", ScriptBackupPath, ScriptPath),
|
||||
fmt.Sprintf("%s restart", ScriptPath),
|
||||
}
|
||||
|
||||
command := strings.Join(commands, " && ")
|
||||
return exec.Command("sh", "-c", command).Run()
|
||||
}
|
||||
|
||||
func (c *Cli) Stop() error {
|
||||
command := fmt.Sprintf("%s stop", ScriptPath)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
return os.Remove(ScriptPath)
|
||||
}
|
||||
|
||||
func (c *Cli) Up() error {
|
||||
command := "tailscale up --accept-dns=false"
|
||||
return exec.Command("sh", "-c", command).Run()
|
||||
}
|
||||
|
||||
func (c *Cli) Down() error {
|
||||
command := "tailscale down"
|
||||
return exec.Command("sh", "-c", command).Run()
|
||||
}
|
||||
|
||||
func (c *Cli) Status() (*TsStatus, error) {
|
||||
command := "tailscale status --json"
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// output is not in standard json format
|
||||
if outputStr := string(output); !strings.HasPrefix(outputStr, "{") {
|
||||
index := strings.Index(outputStr, "{")
|
||||
if index == -1 {
|
||||
return nil, errors.New("unknown output")
|
||||
}
|
||||
|
||||
output = []byte(outputStr[index:])
|
||||
}
|
||||
|
||||
var status TsStatus
|
||||
err = json.Unmarshal(output, &status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (c *Cli) Login() (string, error) {
|
||||
command := "tailscale login --accept-dns=false --timeout=10m"
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = stderr.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = cmd.Run()
|
||||
}()
|
||||
|
||||
reader := bufio.NewReader(stderr)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.Contains(line, "https") {
|
||||
reg := regexp.MustCompile(`\s+`)
|
||||
url := reg.ReplaceAllString(line, "")
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) Logout() error {
|
||||
command := "tailscale logout"
|
||||
return exec.Command("sh", "-c", command).Run()
|
||||
}
|
||||
118
server/service/extensions/tailscale/install.go
Normal file
118
server/service/extensions/tailscale/install.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/utils"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
OriginalURL = "https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
|
||||
Workspace = "/root/.tailscale"
|
||||
)
|
||||
|
||||
func isInstalled() bool {
|
||||
_, err1 := os.Stat(TailscalePath)
|
||||
_, err2 := os.Stat(TailscaledPath)
|
||||
|
||||
return err1 == nil && err2 == nil
|
||||
}
|
||||
|
||||
func install() error {
|
||||
_ = os.MkdirAll(Workspace, 0o755)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(Workspace)
|
||||
}()
|
||||
|
||||
tarFile := fmt.Sprintf("%s/tailscale_riscv64.tgz", Workspace)
|
||||
|
||||
// download
|
||||
if err := download(tarFile); err != nil {
|
||||
log.Errorf("failed to download tailscale: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// decompress
|
||||
dir, err := utils.UnTarGz(tarFile, Workspace)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decompress tailscale: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// move
|
||||
tailscalePath := fmt.Sprintf("%s/tailscale", dir)
|
||||
err = utils.MoveFile(tailscalePath, TailscalePath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to move tailscale: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
tailscaledPath := fmt.Sprintf("%s/tailscaled", dir)
|
||||
err = utils.MoveFile(tailscaledPath, TailscaledPath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to move tailscaled: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("install tailscale successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func download(target string) error {
|
||||
url, err := getDownloadURL()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get Tailscale download url: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Errorf("failed to download Tailscale: %s", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create file: %s", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = out.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to copy response body to file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("download Tailscale successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDownloadURL() (string, error) {
|
||||
resp, err := (&http.Client{}).Get(OriginalURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
|
||||
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp.Request.URL.String(), nil
|
||||
}
|
||||
235
server/service/extensions/tailscale/service.go
Normal file
235
server/service/extensions/tailscale/service.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
const (
|
||||
TailscalePath = "/usr/bin/tailscale"
|
||||
TailscaledPath = "/usr/sbin/tailscaled"
|
||||
ConfigPath = "etc/sysctl.d/99-tailscale.conf"
|
||||
)
|
||||
|
||||
var StateMap = map[string]proto.TailscaleState{
|
||||
"NoState": proto.TailscaleNotRunning,
|
||||
"Starting": proto.TailscaleNotRunning,
|
||||
"NeedsLogin": proto.TailscaleNotLogin,
|
||||
"Running": proto.TailscaleRunning,
|
||||
"Stopped": proto.TailscaleStopped,
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Install(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if !isInstalled() {
|
||||
if err := install(); err != nil {
|
||||
rsp.ErrRsp(c, -1, "install failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("install tailscale successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Uninstall(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
_ = NewCli().Stop()
|
||||
_ = utils.DelGoMemLimit()
|
||||
|
||||
_ = os.Remove(TailscalePath)
|
||||
_ = os.Remove(TailscaledPath)
|
||||
_ = os.Remove(ConfigPath)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("uninstall tailscale successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Start(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Start()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "start failed")
|
||||
log.Errorf("failed to run tailscale start: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.IsGoMemLimitExist() {
|
||||
_ = utils.SetGoMemLimit(50)
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("tailscale start successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Restart(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Restart()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "restart failed")
|
||||
log.Errorf("failed to run tailscale restart: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("tailscale restart successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Stop(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Stop()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "stop failed")
|
||||
log.Errorf("failed to run tailscale stop: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = utils.DelGoMemLimit()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("tailscale stop successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Up(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Up()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "tailscale up failed")
|
||||
log.Errorf("failed to run tailscale up: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("run tailscale up successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Down(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Down()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "tailscale down failed")
|
||||
log.Errorf("failed to run tailscale down: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("run tailscale down successfully")
|
||||
}
|
||||
|
||||
func (s *Service) Login(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
// check tailscale status
|
||||
cli := NewCli()
|
||||
status, err := cli.Status()
|
||||
if err != nil {
|
||||
_ = cli.Start()
|
||||
status, err = cli.Status()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to get tailscale status: %s", err)
|
||||
rsp.ErrRsp(c, -1, "unknown status")
|
||||
return
|
||||
}
|
||||
|
||||
if status.BackendState == "Running" {
|
||||
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{})
|
||||
return
|
||||
}
|
||||
|
||||
// get login url
|
||||
url, err := cli.Login()
|
||||
if err != nil {
|
||||
log.Errorf("failed to run tailscale login: %s", err)
|
||||
rsp.ErrRsp(c, -2, "login failed")
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.IsGoMemLimitExist() {
|
||||
_ = utils.SetGoMemLimit(50)
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{
|
||||
Url: url,
|
||||
})
|
||||
|
||||
log.Debugf("tailscale login url: %s", url)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
err := NewCli().Logout()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "logout failed")
|
||||
log.Errorf("failed to run tailscale logout: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("tailscale logout successfully")
|
||||
}
|
||||
|
||||
func (s *Service) GetStatus(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if !isInstalled() {
|
||||
rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{
|
||||
State: proto.TailscaleNotInstall,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := NewCli().Status()
|
||||
if err != nil {
|
||||
log.Debugf("failed to get tailscale status: %s", err)
|
||||
rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{
|
||||
State: proto.TailscaleNotRunning,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state, ok := StateMap[status.BackendState]
|
||||
if !ok {
|
||||
log.Errorf("unknown tailscale state: %s", status.BackendState)
|
||||
rsp.ErrRsp(c, -1, "unknown state")
|
||||
return
|
||||
}
|
||||
|
||||
ipv4 := ""
|
||||
for _, tailscaleIp := range status.Self.TailscaleIPs {
|
||||
ip := net.ParseIP(tailscaleIp)
|
||||
if ip != nil && ip.To4() != nil {
|
||||
ipv4 = ip.String()
|
||||
}
|
||||
}
|
||||
|
||||
data := proto.GetTailscaleStatusRsp{
|
||||
State: state,
|
||||
IP: ipv4,
|
||||
Name: status.Self.HostName,
|
||||
Account: status.CurrentTailnet.Name,
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &data)
|
||||
log.Debugf("get tailscale status successfully")
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package hid
|
||||
|
||||
const (
|
||||
MouseUp = iota
|
||||
MouseDown
|
||||
MouseMoveAbsolute
|
||||
MouseMoveRelative
|
||||
MouseScroll
|
||||
)
|
||||
|
||||
const (
|
||||
MouseLeft = 1
|
||||
MouseRight = 2
|
||||
MouseWheel = 3
|
||||
)
|
||||
|
||||
const (
|
||||
HidMouseLeft byte = 0x01
|
||||
HidMouseRight byte = 0x02
|
||||
HidMouseWheel byte = 0x04
|
||||
)
|
||||
|
||||
const (
|
||||
ModifierLCtrl byte = 0x01
|
||||
ModifierLShift byte = 0x02
|
||||
ModifierLAlt byte = 0x04
|
||||
ModifierLGUI byte = 0x08
|
||||
ModifierRAlt byte = 0x40
|
||||
)
|
||||
@@ -9,39 +9,13 @@ func (h *Hid) Keyboard(queue <-chan []int) {
|
||||
}
|
||||
|
||||
func (h *Hid) writeKeyboard(event []int) {
|
||||
var data []byte
|
||||
code := byte(event[0])
|
||||
|
||||
if event[0] > 0 {
|
||||
code := byte(event[0])
|
||||
modifier := getModifier(event)
|
||||
data = []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
} else {
|
||||
data = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
var modifier byte = 0x00
|
||||
if code > 0 {
|
||||
modifier = byte(event[1]) | byte(event[2]) | byte(event[3]) | byte(event[4])
|
||||
}
|
||||
|
||||
data := []byte{modifier, 0x00, code, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
h.Write(h.g0, data)
|
||||
}
|
||||
|
||||
func getModifier(event []int) byte {
|
||||
var modifier byte = 0x00
|
||||
|
||||
if event[1] == 1 {
|
||||
modifier |= ModifierLCtrl
|
||||
}
|
||||
|
||||
if event[2] == 1 {
|
||||
modifier |= ModifierLShift
|
||||
}
|
||||
|
||||
if event[3] == 1 {
|
||||
modifier |= ModifierLAlt
|
||||
} else if event[3] == 2 {
|
||||
modifier |= ModifierRAlt
|
||||
}
|
||||
|
||||
if event[4] == 1 {
|
||||
modifier |= ModifierLGUI
|
||||
}
|
||||
|
||||
return modifier
|
||||
}
|
||||
|
||||
@@ -9,9 +9,24 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
MouseUp = iota
|
||||
MouseDown
|
||||
MouseMoveAbsolute
|
||||
MouseMoveRelative
|
||||
MouseScroll
|
||||
)
|
||||
|
||||
var mouseButtonMap = map[byte]bool{
|
||||
0x01: true,
|
||||
0x02: true,
|
||||
0x04: true,
|
||||
}
|
||||
|
||||
func (h *Hid) Mouse(queue <-chan []int) {
|
||||
for event := range queue {
|
||||
h.mouseMutex.Lock()
|
||||
|
||||
switch event[0] {
|
||||
case MouseDown:
|
||||
h.mouseDown(event)
|
||||
@@ -22,25 +37,19 @@ func (h *Hid) Mouse(queue <-chan []int) {
|
||||
case MouseMoveRelative:
|
||||
h.mouseMoveRelative(event)
|
||||
case MouseScroll:
|
||||
h.scroll(event)
|
||||
h.mouseScroll(event)
|
||||
default:
|
||||
log.Debugf("invalid mouse event: %+v", event)
|
||||
}
|
||||
|
||||
h.mouseMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hid) mouseDown(event []int) {
|
||||
var button byte
|
||||
button := byte(event[1])
|
||||
|
||||
switch event[1] {
|
||||
case MouseLeft:
|
||||
button = HidMouseLeft
|
||||
case MouseRight:
|
||||
button = HidMouseRight
|
||||
case MouseWheel:
|
||||
button = HidMouseWheel
|
||||
default:
|
||||
if _, ok := mouseButtonMap[button]; !ok {
|
||||
log.Debugf("invalid mouse button: %+v", event)
|
||||
return
|
||||
}
|
||||
@@ -54,7 +63,7 @@ func (h *Hid) mouseUp() {
|
||||
h.writeWithTimeout(h.g1, data)
|
||||
}
|
||||
|
||||
func (h *Hid) scroll(event []int) {
|
||||
func (h *Hid) mouseScroll(event []int) {
|
||||
direction := 0x01
|
||||
if event[3] > 0 {
|
||||
direction = -0x1
|
||||
|
||||
@@ -3,6 +3,7 @@ package hid
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -10,8 +11,6 @@ import (
|
||||
|
||||
func (s *Service) Reset(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
s.hid.kbMutex.Lock()
|
||||
defer s.hid.kbMutex.Unlock()
|
||||
|
||||
// reset USB
|
||||
f, err := os.OpenFile("/sys/kernel/config/usb_gadget/g0/UDC", os.O_WRONLY, 0644)
|
||||
@@ -43,6 +42,8 @@ func (s *Service) Reset(c *gin.Context) {
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
devices, err := os.ReadDir("/sys/class/udc/")
|
||||
if err != nil {
|
||||
log.Errorf("read udc directory failed: %s", err)
|
||||
@@ -64,7 +65,6 @@ func (s *Service) Reset(c *gin.Context) {
|
||||
rsp.ErrRsp(c, -1, "write to usb gadget file failed")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = f.Close()
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
tailscalePath = "/usr/bin/tailscale"
|
||||
tailscaledPath = "/usr/sbin/tailscaled"
|
||||
|
||||
backendStateRunning = "Running"
|
||||
backendStateNeedsLogin = "NeedsLogin"
|
||||
backendStateStopped = "Stopped"
|
||||
|
||||
responseStateRunning = "running"
|
||||
responseStateNeedsLogin = "notLogin"
|
||||
responseStateStopped = "stopped"
|
||||
responseStateNotInstalled = "notInstall"
|
||||
)
|
||||
|
||||
var backendToResponseStates = map[string]string{
|
||||
backendStateRunning: responseStateRunning,
|
||||
backendStateNeedsLogin: responseStateNeedsLogin,
|
||||
backendStateStopped: responseStateStopped,
|
||||
}
|
||||
|
||||
func stateToJson(state string) string {
|
||||
if resp, ok := backendToResponseStates[state]; ok {
|
||||
return resp
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type TailscaleStatus struct {
|
||||
BackendState string `json:"BackendState"`
|
||||
|
||||
Self struct {
|
||||
HostName string `json:"HostName"`
|
||||
TailscaleIPs []string `json:"TailscaleIPs"`
|
||||
} `json:"Self"`
|
||||
|
||||
CurrentTailnet struct {
|
||||
Name string `json:"Name"`
|
||||
} `json:"CurrentTailnet"`
|
||||
}
|
||||
|
||||
func (s *Service) InstallTailscale(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if exist := isTailscaleExist(); exist {
|
||||
rsp.OkRsp(c)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
downloadUrl = "https://cdn.sipeed.com/nanokvm/resources/tailscale_riscv64.zip"
|
||||
workspace = "/root/.tailscale"
|
||||
)
|
||||
|
||||
var (
|
||||
zipFile = fmt.Sprintf("%s/tailscale_riscv64.zip", workspace)
|
||||
tailscale = fmt.Sprintf("%s/tailscale_riscv64/tailscale", workspace)
|
||||
tailscaled = fmt.Sprintf("%s/tailscale_riscv64/tailscaled", workspace)
|
||||
)
|
||||
|
||||
// download
|
||||
_ = os.MkdirAll(workspace, 0o755)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(workspace)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequest("GET", downloadUrl, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create new request: %s", err)
|
||||
rsp.ErrRsp(c, -1, "request failed")
|
||||
return
|
||||
}
|
||||
|
||||
err = utils.Download(req, zipFile)
|
||||
if err != nil {
|
||||
log.Errorf("download failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "download failed")
|
||||
return
|
||||
}
|
||||
|
||||
// install
|
||||
err = utils.Unzip(zipFile, workspace)
|
||||
if err != nil {
|
||||
log.Errorf("unzip failed: %s", err)
|
||||
rsp.ErrRsp(c, -3, "unzip failed")
|
||||
return
|
||||
}
|
||||
|
||||
err = utils.MoveFile(tailscale, tailscalePath)
|
||||
if err != nil {
|
||||
log.Errorf("rename %s failed: %s", tailscale, err)
|
||||
}
|
||||
err = utils.MoveFile(tailscaled, tailscaledPath)
|
||||
if err != nil {
|
||||
log.Errorf("rename %s failed: %s", tailscaled, err)
|
||||
}
|
||||
|
||||
_ = runTailscale()
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("install tailscaled success")
|
||||
}
|
||||
|
||||
func (s *Service) LoginTailscale(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
status, err := getTailscaleStatus()
|
||||
if err != nil {
|
||||
_ = runTailscale()
|
||||
status, err = getTailscaleStatus()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "tailscale unknown status")
|
||||
return
|
||||
}
|
||||
|
||||
if status.BackendState == backendStateRunning {
|
||||
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{
|
||||
Status: responseStateRunning,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("sh", "-c", "tailscale login --timeout=10m")
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "tailscale login failed")
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = stderr.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = cmd.Run()
|
||||
}()
|
||||
|
||||
url := parseLoginUrl(stderr)
|
||||
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{
|
||||
Status: responseStateNeedsLogin,
|
||||
Url: url,
|
||||
})
|
||||
log.Debugf("tailscale login url: %s", url)
|
||||
}
|
||||
|
||||
func (s *Service) LogoutTailscale(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
cmd := exec.Command("sh", "-c", "tailscale logout")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "tailscale logout failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("tailscale logout success")
|
||||
}
|
||||
|
||||
func (s *Service) GetTailscaleStatus(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
var data proto.GetTailscaleStatusRsp
|
||||
|
||||
if exist := isTailscaleExist(); !exist {
|
||||
data.Status = responseStateNotInstalled
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := getTailscaleStatus()
|
||||
if err != nil {
|
||||
data.Status = responseStateNeedsLogin
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
data.Status = stateToJson(status.BackendState)
|
||||
if data.Status == "" {
|
||||
rsp.ErrRsp(c, -1, "unknown state")
|
||||
return
|
||||
}
|
||||
|
||||
for _, tailscaleIp := range status.Self.TailscaleIPs {
|
||||
ip := net.ParseIP(tailscaleIp)
|
||||
if ip != nil && ip.To4() != nil {
|
||||
data.IP = ip.String()
|
||||
}
|
||||
}
|
||||
|
||||
data.Name = status.Self.HostName
|
||||
data.Account = status.CurrentTailnet.Name
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
log.Debugf("tailscale status: %s", data.Status)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTailscaleStatus(c *gin.Context) {
|
||||
var req proto.UpdateTailscaleStatusReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("tailscale %s", req.Command)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("tailscale %s failed", req.Command)
|
||||
rsp.ErrRsp(c, -2, msg)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := getTailscaleStatus()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -3, "get tailscale status failed")
|
||||
return
|
||||
}
|
||||
|
||||
data := &proto.UpdateTailscaleStatusRsp{}
|
||||
|
||||
switch status.BackendState {
|
||||
case backendStateRunning:
|
||||
data.Status = responseStateRunning
|
||||
case backendStateStopped:
|
||||
data.Status = responseStateStopped
|
||||
default:
|
||||
rsp.ErrRsp(c, -4, "unknown tailscale status")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
log.Debugf("tailscale %s success", req.Command)
|
||||
}
|
||||
|
||||
func isTailscaleExist() bool {
|
||||
_, err1 := os.Stat(tailscalePath)
|
||||
_, err2 := os.Stat(tailscaledPath)
|
||||
|
||||
return err1 == nil && err2 == nil
|
||||
}
|
||||
|
||||
func runTailscale() error {
|
||||
for _, filePath := range []string{tailscalePath, tailscaledPath} {
|
||||
if err := utils.EnsurePermission(filePath, 0o100); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := exec.Command("sh", "-c", "/etc/init.d/S98tailscaled start").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTailscaleStatus() (TailscaleStatus, error) {
|
||||
var status TailscaleStatus
|
||||
|
||||
command := "tailscale status --json"
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Debugf("get tailscale status failed: %s", err)
|
||||
return status, err
|
||||
}
|
||||
|
||||
// delete warning message
|
||||
if str := string(output); !strings.HasPrefix(str, "{") {
|
||||
index := strings.Index(str, "{")
|
||||
if index != -1 {
|
||||
output = []byte(str[index:])
|
||||
}
|
||||
}
|
||||
|
||||
err = json.Unmarshal(output, &status)
|
||||
if err != nil {
|
||||
log.Debugf("unmarshal tailscale status failed: %s", err)
|
||||
return status, err
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func parseLoginUrl(r io.Reader) string {
|
||||
reader := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Errorf("reading line failed: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.Contains(line, "https") {
|
||||
reg := regexp.MustCompile(`\s+`)
|
||||
return reg.ReplaceAllString(line, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
75
server/service/network/wifi.go
Normal file
75
server/service/network/wifi.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
WiFiExistFile = "/etc/kvm/wifi_exist"
|
||||
WiFiSSID = "/etc/kvm/wifi.ssid"
|
||||
WiFiPasswd = "/etc/kvm/wifi.pass"
|
||||
WiFiConnect = "/kvmapp/kvm/wifi_try_connect"
|
||||
WiFiStateFile = "/kvmapp/kvm/wifi_state"
|
||||
)
|
||||
|
||||
func (s *Service) GetWifi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
data := &proto.GetWifiRsp{}
|
||||
|
||||
_, err := os.Stat(WiFiExistFile)
|
||||
if err != nil {
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
data.Supported = true
|
||||
|
||||
content, err := os.ReadFile(WiFiStateFile)
|
||||
if err != nil {
|
||||
rsp.OkRspWithData(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
state := strings.ReplaceAll(string(content), "\n", "")
|
||||
data.Connected = state == "1"
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
log.Debugf("get wifi state: %s", state)
|
||||
}
|
||||
|
||||
func (s *Service) ConnectWifi(c *gin.Context) {
|
||||
var req proto.ConnectWifiReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiSSID, []byte(req.Ssid), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi ssid: %s", err)
|
||||
rsp.ErrRsp(c, -2, "failed to save wifi")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiPasswd, []byte(req.Password), 0o644); err != nil {
|
||||
log.Errorf("failed to save wifi password: %s", err)
|
||||
rsp.ErrRsp(c, -3, "failed to save wifi")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(WiFiConnect, nil, 0o644); err != nil {
|
||||
log.Errorf("failed to connect wifi: %s", err)
|
||||
rsp.ErrRsp(c, -4, "failed to connect wifi")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set wifi successfully: %s", req.Ssid)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
WolHistory = "/etc/kvm/cache/wol"
|
||||
WolMacFile = "/etc/kvm/cache/wol"
|
||||
)
|
||||
|
||||
func (s *Service) WakeOnLAN(c *gin.Context) {
|
||||
@@ -26,34 +27,39 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("ether-wake %s", req.Mac)
|
||||
mac, err := parseMAC(req.Mac)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "invalid MAC address")
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("ether-wake %s", mac)
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Errorf("wake on lan failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, string(output))
|
||||
log.Errorf("failed to wake on lan: %s", err)
|
||||
rsp.ErrRsp(c, -3, string(output))
|
||||
return
|
||||
}
|
||||
|
||||
go saveMac(req.Mac)
|
||||
go saveMac(mac)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("wake on lan %s success", req.Mac)
|
||||
log.Debugf("wake on lan: %s", mac)
|
||||
}
|
||||
|
||||
func (s *Service) GetMac(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
content, err := os.ReadFile(WolHistory)
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "open file error")
|
||||
return
|
||||
}
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
data := &proto.GetMacRsp{
|
||||
Macs: macs,
|
||||
Macs: strings.Split(string(content), "\n"),
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
@@ -68,9 +74,9 @@ func (s *Service) DeleteMac(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(WolHistory)
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", WolHistory, err)
|
||||
log.Errorf("failed to open %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -2, "read failed")
|
||||
return
|
||||
}
|
||||
@@ -85,15 +91,41 @@ func (s *Service) DeleteMac(c *gin.Context) {
|
||||
}
|
||||
|
||||
data := strings.Join(newMacs, "\n")
|
||||
err = os.WriteFile(WolHistory, []byte(data), 0o644)
|
||||
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("write %s failed: %s", WolHistory, err)
|
||||
log.Errorf("failed to write %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("delete mac %s success", req.Mac)
|
||||
log.Debugf("delete wol mac: %s", req.Mac)
|
||||
}
|
||||
|
||||
func parseMAC(mac string) (string, error) {
|
||||
mac = strings.ToUpper(strings.TrimSpace(mac))
|
||||
|
||||
mac = strings.ReplaceAll(mac, "-", "")
|
||||
mac = strings.ReplaceAll(mac, ":", "")
|
||||
mac = strings.ReplaceAll(mac, ".", "")
|
||||
|
||||
matched, err := regexp.MatchString("^[0-9A-F]{12}$", mac)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !matched {
|
||||
return "", fmt.Errorf("invalid MAC address: %s", mac)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for i := 0; i < 12; i += 2 {
|
||||
if i > 0 {
|
||||
result.WriteString(":")
|
||||
}
|
||||
result.WriteString(mac[i : i+2])
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func saveMac(mac string) {
|
||||
@@ -101,15 +133,15 @@ func saveMac(mac string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(WolHistory), 0o644)
|
||||
err := os.MkdirAll(filepath.Dir(WolMacFile), 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("create dir failed: %s", err)
|
||||
log.Errorf("failed to create dir: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(WolHistory, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
file, err := os.OpenFile(WolMacFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("open %s failed: %s", WolHistory, err)
|
||||
log.Errorf("failed to open %s: %s", WolMacFile, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
@@ -119,13 +151,13 @@ func saveMac(mac string) {
|
||||
content := fmt.Sprintf("%s\n", mac)
|
||||
_, err = file.WriteString(content)
|
||||
if err != nil {
|
||||
log.Errorf("write %s failed: %s", WolHistory, err)
|
||||
log.Errorf("failed to write %s: %s", WolMacFile, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func isMacExist(mac string) bool {
|
||||
content, err := os.ReadFile(WolHistory)
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -15,7 +16,11 @@ import (
|
||||
const (
|
||||
imageDirectory = "/data"
|
||||
imageNone = "/dev/mmcblk0p3"
|
||||
cdromFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/cdrom"
|
||||
mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file"
|
||||
removableFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/removable"
|
||||
roFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/ro"
|
||||
roDisk = "/boot/usb.disk0.ro"
|
||||
)
|
||||
|
||||
func (s *Service) GetImages(c *gin.Context) {
|
||||
@@ -61,6 +66,49 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
image = imageNone
|
||||
}
|
||||
|
||||
imageRemovable := "1"
|
||||
|
||||
imageRo := "0"
|
||||
isImageRo, _ := isFlagExist(roDisk)
|
||||
if isImageRo {
|
||||
imageRo = "1"
|
||||
}
|
||||
|
||||
imageLow := strings.ToLower(image)
|
||||
imageCdrom := "0"
|
||||
if strings.HasSuffix(imageLow, ".iso") {
|
||||
imageRo = "1"
|
||||
imageCdrom = "1"
|
||||
}
|
||||
|
||||
// unmount
|
||||
if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil {
|
||||
log.Errorf("unmount file failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "unmount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
// removable flag
|
||||
if err := os.WriteFile(removableFlag, []byte(imageRemovable), 0o666); err != nil {
|
||||
log.Errorf("set removable flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set removable flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// ro flag
|
||||
if err := os.WriteFile(roFlag, []byte(imageRo), 0o666); err != nil {
|
||||
log.Errorf("set ro flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set ro flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom flag
|
||||
if err := os.WriteFile(cdromFlag, []byte(imageCdrom), 0o666); err != nil {
|
||||
log.Errorf("set cdrom flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set cdrom flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// mount
|
||||
if err := os.WriteFile(mountDevice, []byte(image), 0o666); err != nil {
|
||||
log.Errorf("mount file %s failed: %s", image, err)
|
||||
@@ -106,3 +154,18 @@ func (s *Service) GetMountedImage(c *gin.Context) {
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func isFlagExist(flag string) (bool, error) {
|
||||
_, err := os.Stat(flag)
|
||||
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
log.Errorf("check file %s err: %s", flag, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package h264
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
@@ -34,15 +35,29 @@ func Connect(c *gin.Context) {
|
||||
log.Debugf("h264 websocket disconnected")
|
||||
}()
|
||||
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
conf := config.GetInstance()
|
||||
|
||||
var iceServers []webrtc.ICEServer
|
||||
|
||||
if conf.Stun != "" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"stun:" + conf.Stun},
|
||||
})
|
||||
}
|
||||
|
||||
peerConn, err := webrtc.NewPeerConnection(config)
|
||||
if conf.Turn.TurnAddr != "" && conf.Turn.TurnUser != "" && conf.Turn.TurnCred != "" {
|
||||
iceServers = append(iceServers, webrtc.ICEServer{
|
||||
URLs: []string{"turn:" + conf.Turn.TurnAddr},
|
||||
Username: conf.Turn.TurnUser,
|
||||
Credential: conf.Turn.TurnCred,
|
||||
})
|
||||
}
|
||||
|
||||
rtc_config := webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
}
|
||||
|
||||
peerConn, err := webrtc.NewPeerConnection(rtc_config)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create PeerConnection: %s", err)
|
||||
return
|
||||
|
||||
23
server/service/vm/hdmi.go
Normal file
23
server/service/vm/hdmi.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/common"
|
||||
"NanoKVM-Server/proto"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) ResetHdmi(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
vision := common.GetKvmVision()
|
||||
|
||||
vision.SetHDMI(false)
|
||||
time.Sleep(1 * time.Second)
|
||||
vision.SetHDMI(true)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("reset hdmi")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/config"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -16,6 +17,7 @@ var imageVersionMap = map[string]string{
|
||||
"2024-06-23-20-59-2d2bfb.img": "v1.0.0",
|
||||
"2024-07-23-20-18-587710.img": "v1.1.0",
|
||||
"2024-08-08-19-44-bef2ca.img": "v1.2.0",
|
||||
"2024-11-13-09-59-9c961a.img": "v1.3.0",
|
||||
}
|
||||
|
||||
func (s *Service) GetInfo(c *gin.Context) {
|
||||
@@ -91,3 +93,14 @@ func getDeviceKey() string {
|
||||
|
||||
return strings.ReplaceAll(string(content), "\n", "")
|
||||
}
|
||||
|
||||
func (s *Service) GetHardware(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
conf := config.GetInstance()
|
||||
version := conf.Hardware.Version.String()
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetHardwareRsp{
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
|
||||
58
server/service/vm/memory.go
Normal file
58
server/service/vm/memory.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) SetMemoryLimit(c *gin.Context) {
|
||||
var req proto.SetMemoryLimitReq
|
||||
var rsp proto.Response
|
||||
|
||||
err := proto.ParseFormRequest(c, &req)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enabled {
|
||||
err = utils.SetGoMemLimit(req.Limit)
|
||||
} else {
|
||||
err = utils.DelGoMemLimit()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to set memory limit")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debug("set memory limit successful, enabled: %t, limit: %s", req.Enabled, req.Limit)
|
||||
}
|
||||
|
||||
func (s *Service) GetMemoryLimit(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
exist := utils.IsGoMemLimitExist()
|
||||
if !exist {
|
||||
rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{
|
||||
Enabled: false,
|
||||
Limit: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := utils.GetGoMemLimit()
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "failed to get memory limit")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetMemoryLimitRsp{
|
||||
Enabled: true,
|
||||
Limit: limit,
|
||||
})
|
||||
}
|
||||
72
server/service/vm/oled.go
Normal file
72
server/service/vm/oled.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
OLEDExistFile = "/etc/kvm/oled_exist"
|
||||
OLEDSleepFile = "/etc/kvm/oled_sleep"
|
||||
)
|
||||
|
||||
func (s *Service) SetOLED(c *gin.Context) {
|
||||
var req proto.SetOledReq
|
||||
var rsp proto.Response
|
||||
|
||||
if err := proto.ParseFormRequest(c, &req); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
data := []byte(fmt.Sprintf("%d", req.Sleep))
|
||||
err := os.WriteFile(OLEDSleepFile, data, 0o644)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "failed to write data")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("set OLED sleep: %d", req.Sleep)
|
||||
}
|
||||
|
||||
func (s *Service) GetOLED(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if _, err := os.Stat(OLEDExistFile); err != nil {
|
||||
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
|
||||
Exist: false,
|
||||
Sleep: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(OLEDSleepFile)
|
||||
if err != nil {
|
||||
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
|
||||
Exist: true,
|
||||
Sleep: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(string(data))
|
||||
sleep, err := strconv.Atoi(content)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse OLED: %s", err)
|
||||
rsp.ErrRsp(c, -1, "failed to parse OLED config")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, &proto.GetOLEDRsp{
|
||||
Exist: true,
|
||||
Sleep: sleep,
|
||||
})
|
||||
log.Debugf("get OLED config successful, sleep %d", sleep)
|
||||
}
|
||||
@@ -32,7 +32,6 @@ func (s *Service) GetScripts(c *gin.Context) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("get scripts failed: %s", err)
|
||||
rsp.ErrRsp(c, -1, "get scripts failed")
|
||||
return
|
||||
}
|
||||
@@ -41,7 +40,7 @@ func (s *Service) GetScripts(c *gin.Context) {
|
||||
Files: files,
|
||||
})
|
||||
|
||||
log.Debugf("get scripts success")
|
||||
log.Debugf("get scripts total %d", len(files))
|
||||
}
|
||||
|
||||
func (s *Service) UploadScript(c *gin.Context) {
|
||||
|
||||
67
server/service/vm/ssh.go
Normal file
67
server/service/vm/ssh.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"NanoKVM-Server/proto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
SSHScript = "/etc/init.d/S50sshd"
|
||||
SSHStopFlag = "/etc/kvm/ssh_stop"
|
||||
)
|
||||
|
||||
func (s *Service) GetSSHState(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
enabled := isSSHEnabled()
|
||||
rsp.OkRspWithData(c, &proto.GetSSHStateRsp{
|
||||
Enabled: enabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) EnableSSH(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
command := fmt.Sprintf("%s permanent_on", SSHScript)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to run SSH script: %s", err)
|
||||
rsp.ErrRsp(c, -1, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("SSH enabled")
|
||||
}
|
||||
|
||||
func (s *Service) DisableSSH(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
command := fmt.Sprintf("%s permanent_off", SSHScript)
|
||||
err := exec.Command("sh", "-c", command).Run()
|
||||
if err != nil {
|
||||
log.Errorf("failed to run SSH script: %s", err)
|
||||
rsp.ErrRsp(c, -1, "operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("SSH disabled")
|
||||
}
|
||||
|
||||
func isSSHEnabled() bool {
|
||||
_, err := os.Stat(SSHStopFlag)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -2,17 +2,15 @@ package vm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"NanoKVM-Server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,227 +18,93 @@ const (
|
||||
maxMessageSize = 1024
|
||||
)
|
||||
|
||||
type WindowSize struct {
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
||||
type SshClient struct {
|
||||
conn *websocket.Conn
|
||||
addr string
|
||||
user string
|
||||
password string
|
||||
client *ssh.Client
|
||||
session *ssh.Session
|
||||
sessionIn io.WriteCloser
|
||||
sessionOut io.Reader
|
||||
closeSig chan struct{}
|
||||
type WinSize struct {
|
||||
Rows uint16 `json:"rows"`
|
||||
Cols uint16 `json:"cols"`
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: maxMessageSize,
|
||||
WriteBufferSize: maxMessageSize,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
var terminalModes = ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
func (s *SshClient) getWindowSize() (size *WindowSize, err error) {
|
||||
_ = s.conn.SetReadDeadline(time.Now().Add(messageWait))
|
||||
|
||||
msgType, msg, err := s.conn.ReadMessage()
|
||||
func (s *Service) Terminal(c *gin.Context) {
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("ws read message failed: %s", err)
|
||||
log.Errorf("failed to init websocket: %s", err)
|
||||
return
|
||||
}
|
||||
if msgType != websocket.BinaryMessage {
|
||||
log.Errorf("ws message type is not binary")
|
||||
return
|
||||
}
|
||||
|
||||
size = new(WindowSize)
|
||||
if err = json.Unmarshal(msg, size); err != nil {
|
||||
log.Errorf("unmarshal ws message failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SshClient) wsWrite() error {
|
||||
defer func() {
|
||||
s.closeSig <- struct{}{}
|
||||
_ = ws.Close()
|
||||
}()
|
||||
|
||||
cmd := exec.Command("/bin/sh")
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
log.Errorf("failed to start pty: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = ptmx.Close()
|
||||
_ = cmd.Process.Kill()
|
||||
}()
|
||||
|
||||
go wsWrite(ws, ptmx)
|
||||
wsRead(ws, ptmx)
|
||||
}
|
||||
|
||||
// pty to ws
|
||||
func wsWrite(ws *websocket.Conn, ptmx *os.File) {
|
||||
data := make([]byte, maxMessageSize)
|
||||
|
||||
for {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
n, err := s.sessionOut.Read(data)
|
||||
n, err := ptmx.Read(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
_ = s.conn.SetWriteDeadline(time.Now().Add(messageWait))
|
||||
_ = ws.SetWriteDeadline(time.Now().Add(messageWait))
|
||||
|
||||
err = s.conn.WriteMessage(websocket.BinaryMessage, data[:n])
|
||||
err = ws.WriteMessage(websocket.BinaryMessage, data[:n])
|
||||
if err != nil {
|
||||
log.Errorf("write ws message failed: %s", err)
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SshClient) wsRead() error {
|
||||
defer func() {
|
||||
s.closeSig <- struct{}{}
|
||||
}()
|
||||
|
||||
// ws to pty
|
||||
func wsRead(ws *websocket.Conn, ptmx *os.File) {
|
||||
var zeroTime time.Time
|
||||
_ = s.conn.SetReadDeadline(zeroTime)
|
||||
_ = ws.SetReadDeadline(zeroTime)
|
||||
|
||||
for {
|
||||
msgType, connReader, err := s.conn.NextReader()
|
||||
msgType, p, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
if msgType != websocket.BinaryMessage {
|
||||
_, err = io.Copy(s.sessionIn, connReader)
|
||||
if err != nil {
|
||||
log.Errorf("copy ws message failed: %s", err)
|
||||
return err
|
||||
|
||||
// resize message
|
||||
if msgType == websocket.BinaryMessage {
|
||||
var winSize WinSize
|
||||
if err := json.Unmarshal(p, &winSize); err == nil {
|
||||
_ = pty.Setsize(ptmx, &pty.Winsize{
|
||||
Rows: winSize.Rows,
|
||||
Cols: winSize.Cols,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
data := make([]byte, maxMessageSize)
|
||||
n, err := connReader.Read(data)
|
||||
_, err = ptmx.Write(p)
|
||||
if err != nil {
|
||||
log.Errorf("read ws message failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var size WindowSize
|
||||
err = json.Unmarshal(data[:n], &size)
|
||||
if err != nil {
|
||||
log.Errorf("unmarshal ws message failed: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.session.WindowChange(size.Height, size.Width)
|
||||
if err != nil {
|
||||
log.Errorf("ws window change failed: %s", err)
|
||||
return err
|
||||
log.Errorf("failed to write to pty: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SshClient) bridgeWSAndSSH() {
|
||||
defer func() {
|
||||
_ = s.conn.Close()
|
||||
if r := recover(); r != nil {
|
||||
log.Debugf("terminal recover: %s", r)
|
||||
}
|
||||
}()
|
||||
|
||||
size, err := s.getWindowSize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auth := ssh.Password(s.password)
|
||||
config := &ssh.ClientConfig{
|
||||
User: s.user,
|
||||
Auth: []ssh.AuthMethod{auth},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
s.client, err = ssh.Dial("tcp", s.addr, config)
|
||||
if err != nil {
|
||||
// log.Errorf("init ssh failed: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = s.client.Close()
|
||||
}()
|
||||
|
||||
s.session, err = s.client.NewSession()
|
||||
if err != nil {
|
||||
log.Errorf("open ssh session failed: %s", err)
|
||||
return
|
||||
}
|
||||
defer s.session.Close()
|
||||
|
||||
s.session.Stderr = os.Stderr
|
||||
s.sessionOut, err = s.session.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Errorf("open ssh session out failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.sessionIn, err = s.session.StdinPipe()
|
||||
if err != nil {
|
||||
log.Errorf("open ssh session in failed: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = s.sessionIn.Close()
|
||||
}()
|
||||
|
||||
if err = s.session.RequestPty("xterm", size.Height, size.Width, terminalModes); err != nil {
|
||||
log.Errorf("oen session request pty failed: %s", err)
|
||||
return
|
||||
}
|
||||
if err = s.session.Shell(); err != nil {
|
||||
log.Errorf("open ssh shell failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = s.wsRead()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = s.wsWrite()
|
||||
}()
|
||||
|
||||
<-s.closeSig
|
||||
}
|
||||
|
||||
func (s *Service) Terminal(c *gin.Context) {
|
||||
user := c.Query("u")
|
||||
if user == "" {
|
||||
user = "root"
|
||||
}
|
||||
|
||||
password, _ := utils.Decrypt(c.Query("t"))
|
||||
if password == "" {
|
||||
password = "root"
|
||||
}
|
||||
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Errorf("init websocket failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
sshClient := &SshClient{
|
||||
conn: conn,
|
||||
addr: "127.0.0.1:22",
|
||||
user: user,
|
||||
password: password,
|
||||
closeSig: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
go sshClient.bridgeWSAndSSH()
|
||||
}
|
||||
|
||||
@@ -7,30 +7,24 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const EncryptSecretKey = "nanokvm-sipeed-2024"
|
||||
// SecretKey is only used to prevent the data from being transmitted in plaintext.
|
||||
const SecretKey = "nanokvm-sipeed-2024"
|
||||
|
||||
func Decrypt(ciphertext string) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("decrypt failed: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if ciphertext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
decrypt := aes256.Decrypt(ciphertext, EncryptSecretKey)
|
||||
|
||||
decrypt := aes256.Decrypt(ciphertext, SecretKey)
|
||||
return decrypt, nil
|
||||
}
|
||||
|
||||
func DecodeDecrypt(ciphertext string) (string, error) {
|
||||
decode, err := url.QueryUnescape(ciphertext)
|
||||
func DecodeDecrypt(data string) (string, error) {
|
||||
ciphertext, err := url.QueryUnescape(data)
|
||||
if err != nil {
|
||||
log.Errorf("decode ciphertext failed: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Decrypt(decode)
|
||||
return Decrypt(ciphertext)
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ func Download(req *http.Request, target string) error {
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" && contentType != "application/zip" {
|
||||
log.Debugf("unexpected content-type, it should be either octet-stream or zip, but got: %s", contentType)
|
||||
if contentType != "application/octet-stream" && contentType != "application/zip" && contentType != "application/gzip" {
|
||||
log.Debugf("unexpected content-type, it should be either octet-stream or (g)zip, but got: %s", contentType)
|
||||
return errors.New("unsupported content type")
|
||||
}
|
||||
|
||||
|
||||
77
server/utils/memory.go
Normal file
77
server/utils/memory.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const GoMemLimitFile = "/etc/kvm/GOMEMLIMIT"
|
||||
|
||||
func InitGoMemLimit() {
|
||||
if !IsGoMemLimitExist() {
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := GetGoMemLimit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
debug.SetMemoryLimit(limit * 1024 * 1024)
|
||||
log.Debugf("set GOMEMLIMIT to %d MB", limit)
|
||||
}
|
||||
|
||||
func SetGoMemLimit(limit int64) error {
|
||||
memoryLimit := max(limit, 50)
|
||||
debug.SetMemoryLimit(memoryLimit * 1024 * 1024)
|
||||
|
||||
log.Debugf("set GOMEMLIMIT to %d MB", limit)
|
||||
|
||||
data := []byte(fmt.Sprintf("%d", limit))
|
||||
err := os.WriteFile(GoMemLimitFile, data, 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write GOMEMLIMIT: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetGoMemLimit() (int64, error) {
|
||||
data, err := os.ReadFile(GoMemLimitFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read GOMEMLIMIT: %s", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(string(data))
|
||||
limit, err := strconv.ParseInt(content, 10, 64)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse GOMEMLIMIT: %s", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
func DelGoMemLimit() error {
|
||||
debug.SetMemoryLimit(1024 * 1024 * 1024)
|
||||
|
||||
err := os.Remove(GoMemLimitFile)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete GOMEMLIMIT: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsGoMemLimitExist() bool {
|
||||
_, err := os.Stat(GoMemLimitFile)
|
||||
return err == nil
|
||||
}
|
||||
82
server/utils/untar.go
Normal file
82
server/utils/untar.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func UnTarGz(srcFile string, destDir string) (string, error) {
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fr, err := os.Open(srcFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = fr.Close()
|
||||
}()
|
||||
|
||||
gr, err := gzip.NewReader(fr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = gr.Close()
|
||||
}()
|
||||
|
||||
tr := tar.NewReader(gr)
|
||||
|
||||
targetFile := ""
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if targetFile == "" {
|
||||
parts := strings.Split(header.Name, "/")
|
||||
if len(parts) > 0 {
|
||||
targetFile = filepath.Join(destDir, parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
filename := filepath.Join(destDir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(filename, os.FileMode(header.Mode)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
case tar.TypeReg:
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(file, tr); err != nil {
|
||||
_ = file.Close()
|
||||
return "", err
|
||||
}
|
||||
_ = file.Close()
|
||||
|
||||
case tar.TypeSymlink:
|
||||
if err := os.Symlink(header.Linkname, filename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targetFile, nil
|
||||
}
|
||||
8
support/README.md
Normal file
8
support/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## If you want to compile kvm_system yourself, please follow the steps below:
|
||||
|
||||
1. Clone the MaixCDK repository: `git clone https://github.com/sipeed/MaixCDK.git`
|
||||
2. Improve the Compilation Environment, refer to [here](https://github.com/sipeed/MaixCDK/tree/main/docs/doc_zh#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
3. Copy `./kvm_system` to `~/MaixCDK` and open it in terminal
|
||||
4. Execute `maixcdk build` and Select `maixcam` to complete the compilation
|
||||
5. Replace `/kvmapp/kvm_system/kvm_system` in the NanoKVM system with `dist/kvm_system_release/kvm_system`
|
||||
6. Execute: `/etc/init.d/S95nanokvm restart` on NanoKVM
|
||||
28
support/kvm_system/main/CMakeLists.txt
Normal file
28
support/kvm_system/main/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
list(APPEND ADD_INCLUDE
|
||||
"include"
|
||||
"lib/libqr"
|
||||
"lib/system_ctrl"
|
||||
"lib/system_state"
|
||||
"lib/system_init"
|
||||
"lib/oled_ctrl"
|
||||
"lib/oled_ui"
|
||||
"lib/hdmi"
|
||||
)
|
||||
|
||||
list(APPEND ADD_PRIVATE_INCLUDE "")
|
||||
|
||||
append_srcs_dir(ADD_SRCS
|
||||
"src"
|
||||
"lib/libqr"
|
||||
"lib/system_ctrl"
|
||||
"lib/system_state"
|
||||
"lib/system_init"
|
||||
"lib/oled_ctrl"
|
||||
"lib/oled_ui"
|
||||
"lib/hdmi"
|
||||
) # append source file in src dir to var ADD_SRCS
|
||||
|
||||
list(APPEND ADD_REQUIREMENTS basic peripheral)
|
||||
|
||||
register_component()
|
||||
0
support/kvm_system/main/Kconfig
Normal file
0
support/kvm_system/main/Kconfig
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user