Compare commits

...

52 Commits

Author SHA1 Message Date
wj-xiao
0dbf8c007f fix: adjust condition for password update notification
fix: correct formatting error in language file
perf: increase update wait time to 10s (from 6s)
refactor: update libkvm.so and libkvm_mmf.so libraries
2025-02-17 16:45:54 +08:00
wenjie
a24c1b0cef Merge pull request #318 from bilogic/main
refactored for readability
2025-02-17 10:24:03 +08:00
wenjie
42dc6ad079 Merge pull request #317 from protonchang/zhtw-translation
Update zhTW Translation
2025-02-17 10:23:25 +08:00
wenjie
d670cf19a1 Merge pull request #315 from forumi0721/main
Update korean translations
2025-02-17 10:22:42 +08:00
wenjie
7fe7c49e65 Merge pull request #312 from li20034/fix_password_updated_check
Fix password updated check
2025-02-17 10:21:33 +08:00
bilogic
5c1e617862 refactored for readability 2025-02-16 14:33:24 +08:00
protonchang
9ff9d59aa5 Update zhTW Translation 2025-02-16 02:45:18 +08:00
forumi0721
7556d154d3 Update korean translations 2025-02-16 02:06:39 +09:00
Zonggao Li
df808da5d6 Fix password updated check 2025-02-14 23:50:44 -08:00
Caize Wu
06bd869fb1 Update README.md 2025-02-14 15:48:25 +08:00
wj-xiao
7343ca3811 update README and CHANGELOG 2025-02-14 14:35:42 +08:00
wj-xiao
6eb4a4ea62 update version to 2.1.6
feat: support downloading image from online URL
feat: add keyboard shortcut Ctrl+Alt+Del
fix: fix the CSRF issue
perf: add an option to configure custom ICE servers
perf: removed unnecessary modifications to DNS configuration
perf: add an SSH enable/disable toggle in the web UI
perf: add a Tailscale enable/disable toggle in the web UI
perf: download Tailscale installation package from the official source
perf: automatic enable/disable GOMEMLIMIT on tailscale start/stop
perf: add JWT configuration
perf: implement secure password storage using bcrypt hashing
perf: implement integrity checks for online updates
refactor: refactor HDMI module and remove the dependency libmaixcam_lib.so
refactor: web terminal use pty instead of SSH
refactor: move Tailscale APIs from the network module to the extensions module
2025-02-14 14:18:13 +08:00
wj-xiao
9caf34e641 Upgrade vite 2025-02-07 10:21:25 +08:00
wj-xiao
b92bb0c1cb Fix merge conflict 2025-02-07 10:19:30 +08:00
wenjie
7da5194e90 Merge pull request #299 from protonchang/zhtw-translation
Update zhTW Translation
2025-02-06 17:47:38 +08:00
wenjie
b8c34d04bf Merge pull request #288 from forumi0721/main
Update korean translations
2025-02-06 17:46:56 +08:00
wenjie
ab3d980c4d Merge branch 'main' into main 2025-02-06 17:46:44 +08:00
wenjie
a32130348d Merge pull request #285 from VMFortress/custom_ice_servers
Add configuration option for custom ICE servers
2025-02-06 17:45:11 +08:00
wenjie
74917e321f Merge pull request #278 from CaffeeLake/ctrlaltdel
Add: Simple Ctrl+Alt+Del button
2025-02-06 17:44:32 +08:00
wj-xiao
07c3c5b61f add roadmap 2025-02-06 17:42:29 +08:00
Caize Wu
a0862429b9 Update README.md 2025-02-06 15:15:27 +08:00
protonchang
f36ce20df9 Update zhTW Translation 2025-02-06 13:23:35 +08:00
forumi0721
fdd4a9c6d1 Update korean translations 2025-01-29 21:41:04 +09:00
vmfortress
840341e331 Update server docs for custom ICE server config 2025-01-28 13:29:22 -05:00
vmfortress
c9310a319a Add configuration option for custom ICE servers 2025-01-28 13:18:38 -05:00
CaffeeLake
ae204b418f Add: Simple Ctrl+Alt+Del button 2025-01-26 01:40:20 +09:00
wj-xiao
97a9b376c7 update image url 2025-01-22 17:43:52 +08:00
wenjie
54515f6b96 Merge pull request #273 from benrose258/patch-1
Update docs to reflect 1.3.0 firmware release
2025-01-22 10:57:33 +08:00
Ben Rose
cffb85de49 Update to reflect 1.3.0 firmware release 2025-01-21 21:34:15 -05:00
wenjie
c0d9d167a7 Merge pull request #268 from scpcom/storage-device-flags
server: storage: set device flags
2025-01-21 11:48:13 +08:00
wenjie
cef6a141c1 Merge pull request #267 from scpcom/download-image-fixups
Download image fixups
2025-01-21 11:05:24 +08:00
scpcom
29fd3ba9d1 server: storage: set device flags 2025-01-19 00:10:57 +01:00
scpcom
7a9affced2 server: download: define proto 2025-01-18 18:26:35 +01:00
scpcom
83a65559fb web: download: fix response usage 2025-01-18 18:25:42 +01:00
scpcom
68913d2185 web: download: fix interval usage 2025-01-18 18:23:56 +01:00
scpcom
901a0380d7 web: download: remove unused rsp, err and List 2025-01-18 18:23:51 +01:00
BuGu
1db304a5f3 Update support 2025-01-17 19:14:59 +08:00
wenjie
7fe3447a83 Merge pull request #252 from Itxaka/download_image
Add a download action in the menu
2025-01-16 11:33:24 +08:00
wj-xiao
85f6447a16 update version to 2.1.5
- feat: add HDMI reset for NanoKVM-PCIe
- fix: HID reset
- attempt to fix Tailscale OOM
2025-01-14 17:31:01 +08:00
Itxaka
69243e5ebb Merge branch 'main' into download_image 2025-01-10 13:35:28 +01:00
wj-xiao
d7ca7c453d update version to 2.1.4
- changing password will modify both the web user and the system root user
- refactor menu bar
- add oled automatic sleep
- support setting GOMEMLIMIT, and automatically enabled when tailscale is logged in
- some other bug fixes and optimizations
2025-01-10 10:31:38 +08:00
Itxaka
d5eb644baa Add a download action in the menu
Signed-off-by: Itxaka <itxaka@kairos.io>
2025-01-09 11:40:30 +01:00
wj-xiao
26078fe46e fix keyboard incomplete modifier keys
- add api to get WiFi information
- delete field Hardware from application information
- update some keyboard and mouse mapping codes
2024-12-27 18:31:02 +08:00
wenjie
93ce6009c6 Merge pull request #202 from eltociear/add-japanese-readme
docs: add Japanese README files
2024-12-17 09:53:02 +08:00
Ikko Eltociear Ashimine
2cae9e4336 docs: add Japanese README files
I created Japanese translated READMEs.
2024-12-14 17:51:53 +09:00
wenjie
097078f93c Merge pull request #200 from DrEVILish/patch-1
Update README.md
2024-12-13 10:26:08 +08:00
Ryan Freeman
52937df57d Update README.md
Update the English Readme for NanoKVM
2024-12-10 18:39:35 +00:00
wj-xiao
5a39562f2d Support wifi configuration
- update config
- add so libraries for compilation
2024-12-04 18:29:43 +08:00
wenjie
a09ed2dc1f Merge pull request #165 from vincent-chang-rightfighter/main
Add support for Traditional Chinese (zh_tw)
2024-11-26 10:57:30 +08:00
wenjie
3bd52590ff Merge pull request #161 from jonaw/master
Add support for Norwegian bokmål
2024-11-26 10:52:14 +08:00
vincent
fceac7d3b1 Add support for Tradionnal Chinese(zh_tw) 2024-11-24 01:54:55 +08:00
Jon Are Wisting
0ffc78a747 Add support for Norwegian bokmål
Norwegian bokmål is one of Norway's two written languages, designated with the code 'nb' in ISO 639-1
2024-11-21 19:48:04 +01:00
265 changed files with 20379 additions and 9451 deletions

54
CHANGELOG.md Normal file
View File

@@ -0,0 +1,54 @@
## 2.1.6 [6eb4a4e](https://github.com/sipeed/NanoKVM/commit/6eb4a4ea6254f465a47f9881d13934c686649061) (2025-02-14)
* feat: support downloading image from online URL (thanks to [@Itxaka](https://github.com/Itxaka))
* feat: add keyboard shortcut `Ctrl+Alt+Del` (thanks to [@CaffeeLake](https://github.com/CaffeeLake))
* fix: fix the CSRF issue
* perf: add an option to configure custom ICE servers (thanks to [@VMFortress](https://github.com/VMFortress))
* perf: removed unnecessary modifications to DNS configuration
* perf: add an SSH enable/disable toggle in the web UI
* perf: add a Tailscale enable/disable toggle in the web UI
* perf: download Tailscale installation package from the official source
* perf: automatic enable/disable GOMEMLIMIT on tailscale start/stop
* perf: add JWT configuration
* secretKey: customize secret key. If empty, generated a random 64-byte secret key by default
* refreshTokenDuration: customize token expiration time
* revokeTokensOnLogout: invalidate all JWT tokens on logout
* perf: implement secure password storage using bcrypt hashing
* perf: implement integrity checks for online updates
* refactor: refactor HDMI module and remove the dependency `libmaixcam_lib.so`
* refactor: web terminal use pty instead of SSH
* refactor: move Tailscale APIs from the `network` module to the `extensions` module
## 2.1.5 [85f6447](https://github.com/sipeed/NanoKVM/commit/85f6447a16cc2591c6459b7d3dfda4d4cb75e98c) (2025-01-14)
* feat: add HDMI reset for NanoKVM-PCIe
* fix: remove unnecessary lock acquisition during HID reset
* refactor: refactor Tailscale
## 2.1.4 [d7ca7c4](https://github.com/sipeed/NanoKVM/commit/d7ca7c453d821ad099bf79b463969419041279cb) (2025-01-10)
* feat: support configuring OLED sleep settings
* feat: support setting the `GOMEMLIMIT` environment variable
* fix: fix Wi-Fi configuration
* perf: password changes now update both the web user and the system root user
* perf: add MAC address verification for Wake-on-LAN
* refactor: a lot update to web UI
* refactor: refactor Tailscale
## 2.1.3 [26078fe](https://github.com/sipeed/NanoKVM/commit/26078fe46e43d4543d7b09901b4992e4fbe4f01f) (2024-12-27)
* feat: add API to retrieve Wi-Fi information
* fix: fix keyboard modifier keys
* fix: update keyboard and mouse HID codes
* fix: update hardware version information
## 2.1.2 [5a39562](https://github.com/sipeed/NanoKVM/commit/5a39562f2d32695933f4e7e86866136236cc9903) (2024-12-04)
* feat: add hardware version to configuration
* feat: add Wi-Fi configuration support for NanoKVM-PCIe
* perf: update web UI
* chore: add dependency libraries
## 2.1.1 [74a303b](https://github.com/sipeed/NanoKVM/commit/74a303bd5cbb58f9d8ddd81abaaf4919dbbfb71b) (2024-11-06)
* feat: support h264

View File

@@ -3,25 +3,31 @@ NanoKVM
<div align="center">
![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_3.jpg)
![](https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM_3.png)
<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.14
Newest APP v2.1.6 add most security enhancements&bug fix, recommend regular users upgrade to this version or newer.
### Update 10.18
### 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 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.
@@ -42,7 +48,7 @@ The NanoKVM Full is a full version with a sophisticated case and complete access
| --- | --- | --- | --- |
| Compute Units | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
| Resolution | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
| Video encoding | MJPEG, H264(developing) | MJPEG, H264(developing) | MJPEG, H264 |
| Video encoding | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
| Video delay | 90~230ms | 90~230ms | 100~230ms |
| UEFI/BIOS | ✓ | ✓ | ✓ |
| Virtual HID | ✓ | ✓ | ✓ |
@@ -70,23 +76,48 @@ 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:**
- [x] 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~~
- [x] Add JWT configuration options to the configuration file
- [x] Secure password storage with bcrypt
**Distribution:**
- [x] Distribute applications via GitHub
- [x] Implement integrity checks for online updates
- [ ] Support offline updates
**Bug Fixes:**
- [x] Fix the DNS issue
- [x] Fix the CSRF vulnerability
**Features**
- [ ] [79](https://github.com/sipeed/NanoKVM/issues/79) Support Zerotier
- [ ] [99](https://github.com/sipeed/NanoKVM/issues/99) Support WireGuard
- [ ] [249](https://github.com/sipeed/NanoKVM/issues/249) Add Mouse Jiggler
## Where to buy
* [Aliexpress(global except USA&Russia)](https://www.aliexpress.com/item/1005007369816019.html)
* [淘宝](https://item.taobao.com/item.htm?id=811206560480)
* [Preorder (anyother country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm)
## Community
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
* QQ group: 703230713

87
README_JA.md Normal file
View File

@@ -0,0 +1,87 @@
# NanoKVM
<div align="center">
![](https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM_3.png)
<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は完全版で、洗練されたケースと完全なアクセサリーを備え、起動時にすぐに使用できるシステムミラーカードが内蔵されており、個人ユーザーに推奨されます。
![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg)
## 技術仕様
| 製品 | 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 |
![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_2.jpg)
## ハードウェアプラットフォーム 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

View File

@@ -3,14 +3,14 @@ NanoKVM
<div align="center">
![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_3.jpg)
![](https://wiki.sipeed.com/hardware/assets/NanoKVM/introduce/NanoKVM_3.png)
<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>
@@ -33,7 +33,7 @@ NanoKVM Full 为完整版配置,带精致外壳和完整配件,内置开机
| --- | --- | --- | --- |
| 计算单元 | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) |
| 分辨率 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps |
| 视频编码 | MJPEG, H264(developing) | MJPEG, H264(developing) | MJPEG, H264 |
| 视频编码 | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 |
| 视频延迟 | 90~230ms | 90~230ms | 100~230ms |
| UEFI/BIOS | ✓ | ✓ | ✓ |
| 模拟USB键鼠 | ✓ | ✓ | ✓ |
@@ -78,6 +78,3 @@ NanoKVM 镜像在LicheeRV Nano SDK 和 MaixCDK 基础上构建,可以兼容使
* [MaixHub Discussion](https://maixhub.com/discussion/nanokvm)
* QQ group: 703230713

View File

@@ -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
View 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` を実行してサービスを再起動します。

View File

@@ -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` 重启服务。

View File

@@ -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() {

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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
View 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)
}

View File

@@ -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:"-"`

Binary file not shown.

BIN
server/dl_lib/libaacdec2.so Normal file

Binary file not shown.

BIN
server/dl_lib/libaacenc2.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libae.so Normal file

Binary file not shown.

BIN
server/dl_lib/libaf.so Normal file

Binary file not shown.

BIN
server/dl_lib/libawb.so Normal file

Binary file not shown.

BIN
server/dl_lib/libcli.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libcvi_bin.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libcvi_ive.so Normal file

Binary file not shown.

BIN
server/dl_lib/libcvi_ssp.so Normal file

Binary file not shown.

BIN
server/dl_lib/libcvi_vqe.so Normal file

Binary file not shown.

BIN
server/dl_lib/libdnvqe.so Normal file

Binary file not shown.

BIN
server/dl_lib/libini.so Normal file

Binary file not shown.

BIN
server/dl_lib/libisp.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libkvm_mmf.so Normal file

Binary file not shown.

BIN
server/dl_lib/libmipi_tx.so Normal file

Binary file not shown.

BIN
server/dl_lib/libmisc.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libosdc.so Normal file

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libsys.so Normal file

Binary file not shown.

Binary file not shown.

BIN
server/dl_lib/libvdec.so Normal file

Binary file not shown.

BIN
server/dl_lib/libvenc.so Normal file

Binary file not shown.

BIN
server/dl_lib/libvpu.so Normal file

Binary file not shown.

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}()
}

View File

@@ -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)

View File

@@ -4,7 +4,3 @@ type GetVersionRsp struct {
Current string `json:"current"`
Latest string `json:"latest"`
}
type GetLibRsp struct {
Exist bool `json:"exist"`
}

View File

@@ -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
View 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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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
View 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
}

View 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
}

View File

@@ -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
}

View File

@@ -37,4 +37,6 @@ func server(r *gin.Engine) {
networkRouter(r)
hidRouter(r)
wsRouter(r)
downloadRouter(r)
extensionsRouter(r)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{}

View 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
}

View File

@@ -1,33 +1,28 @@
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"
type Account struct {
Username string `json:"username"`
Password string `json:"password"`
Password string `json:"password"` // should be named HashedPassword for clarity
}
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,26 +32,18 @@ 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, hashedPassword string) error {
account, err := json.Marshal(&Account{
Username: username,
Password: password,
Password: hashedPassword,
})
if err != nil {
log.Errorf("failed to marshal account information to json: %s", err)
@@ -77,3 +64,50 @@ func setAccount(username string, password string) error {
return nil
}
func CompareAccount(username string, plainPassword string) bool {
account, err := GetAccount()
if err != nil {
return false
}
if username != account.Username {
return false
}
hashedPassword, err := utils.DecodeDecrypt(plainPassword)
if err != nil || hashedPassword == "" {
return false
}
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(hashedPassword))
if err != nil {
// Compatible with old versions
accountHashedPassword, _ := utils.DecodeDecrypt(account.Password)
if accountHashedPassword == hashedPassword {
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 {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
return &Account{
Username: "admin",
Password: string(hashedPassword),
}
}

View File

@@ -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")
}

View 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")
}

View File

@@ -0,0 +1,124 @@
package auth
import (
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"errors"
"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{
// If the hash is not valid, still assume it's not updated
// The error we want to see is password and hash not matching
IsUpdated: errors.Is(err, bcrypt.ErrMismatchedHashAndPassword),
})
}
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
}

View 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
}

View 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()
}

View 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
}

View 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")
}

View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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, "")
}
}
}

View 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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View 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")
}

View File

@@ -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,
})
}

View 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
View 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)
}

View File

@@ -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
View 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
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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
View 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
View 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
View 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

View 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()

Some files were not shown because too many files have changed in this diff Show More