diff --git a/README.md b/README.md index 3ec4faf..d72e8fc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# [C/S项目]神奇分享_**MagicShare**(中文说明) +# 神奇分享_**MagicShare**(中文说明) + +[**English**](./README_EN.md) --- Website: -[[C/S项目]神奇分享_MagicShare | ZZHow](https://www.zzhow.com/MagicShare) +[神奇分享_MagicShare(中文说明) | ZZHow](https://www.zzhow.com/MagicShare) Source Code: @@ -26,13 +28,13 @@ MagicShare 是一款跨平台的内网文件分享工具,发送方使用桌面 ![TechnicalRoute.png](./TechnicalRoute.png) -- 编程语言:Java 和 JavaScript +- 编程语言:**Java** 和 **JavaScript** --- ## 许可证 -该项目根据 GNU 通用公共许可证 v3.0 获得许可 - 有关详细信息,请参阅 [LICENSE](./LICENSE) 文件。 +该项目根据 GNU 通用公共许可证 v3.0 获得许可 - 有关详细信息,请参阅 [LICENSE](https://github.com/ZZHow1024/MagicShare/blob/main/LICENSE) 文件。 --- @@ -70,6 +72,11 @@ https://github.com/ZZHow1024/MagicShare/releases - 下载对应的文件。 - Linux 和 macOS 需要执行安装操作后再运行,Windows 可直接运行 .zip 压缩包中的 .exe 可执行程序或选择 .exe 安装包与 .msi 安装包执行安装操作,.jar 包可直接通过 `java -jar` 命令运行。 - 启动 MagicShare 并认真阅读启动页说明,同意 “用户许可协议” 可继续使用。 +- 在 MagicShare 的主界面右下方可以选择语言。 + - 当前支持中文(简体 / 繁体)与英文。 +- 选择是否启用连接密码 + - 若希望启用连接密码,需勾选“启用密码”并自定义 3~10 位的密码。 + - 若不希望启用连接密码,需取消勾选“启用密码”。 - 自定义端口后单击 “启动服务” 按钮 - 若提示 “启动成功”,则表示服务正常启动,可将 ”分享URL“ 提供给接收方。 - 若提示 ”端口被占用“,请尝试更换端口号。 @@ -78,6 +85,7 @@ https://github.com/ZZHow1024/MagicShare/releases - 方式一:拖拽待分享的文件/文件夹至软件主界面上半部分。 - 方式二:单击 “选择文件夹” 按钮选择待分享的文件夹。 - 方式三:在 “分享的文件/文件夹” 文本输入框中输入待分享的文件/文件夹路径,按下 “Enter” 键。 +- 在软件右上方可查看当前连接数。 - 按下 “停止服务” 按钮可立即终止分享。 - 按下 “清空分享列表” 按钮可立即清空分享列表。 @@ -85,6 +93,8 @@ https://github.com/ZZHow1024/MagicShare/releases - 打开浏览器,访问 ”分享URL“。 - 启动 MagicShare 并认真阅读启动页说明,同意 “用户许可协议” 可继续使用。 +- 在 MagicShare 的主界面右下方可以选择语言。 + - 当前支持中文(简体 / 繁体)与英文。 - 下载文件 - 单击 ”快速下载“ 使用浏览器下载器通过 HTTP 协议快速下载文件。 - 单击 ”加密下载“ 使用 MagicShare 加密下载器通过 WebSocket 协议并使用 RSA+AES 混合加密下载文件,不支持同时加密下载多个文件。 @@ -96,8 +106,8 @@ https://github.com/ZZHow1024/MagicShare/releases 该项目需要以下库: -- [**Vue.js**](https://github.com/vuejs) 及配套组件:用于构建 Web 前端程序。 -- [**Spring Boot**](https://github.com/spring-projects/spring-boot) 及配套组件:用于构建 Web 后端程序。 +- [**Vue.js**](https://github.com/vuejs)** 及配套组件**:**用于构建 Web 前端程序。 +- [**Spring Boot**](https://github.com/spring-projects/spring-boot)** 及配套组件:用于构建 Web 后端程序。 - [**OpenJFX**](https://openjfx.io/):用于构建图形用户界面的 JavaFX 库。 --- @@ -110,15 +120,33 @@ https://github.com/ZZHow1024/MagicShare/releases - Web 网页下载文件。 - 支持通过 HTTP 协议快速下载文件 - 支持通过 WebSocket 协议并使用 RSA+AES 混合加密下载文件 +- MagicShare2.0.0 + - 当前连接数显示。 + - 自定义连接密码。 + - 支持多语言。 + - 中文(简体/繁体) + - 英文 --- ## 各版本主界面 -![MagicShare1.0.0-Desktop](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fcc029b9b-e911-4cd4-b9c7-4b0853fa6d04%2FMagicShare1.0.0-Desktop.png?table=block&id=17fe64bd-e40f-8066-8c2d-f5f8b1ab0a23&t=17fe64bd-e40f-8066-8c2d-f5f8b1ab0a23&width=707&cache=v2) +### MagicShare2.0.0 + +![MagicShare2.0.0-Desktop-ZH](https://lively-brook-dc1.notion.site/image/attachment%3A138de23b-f7c8-4c8e-8d39-7aec8d577d4d%3AMagicShare2.0.0-Desktop-ZH.png?table=block&id=24e96980-87c2-4fc8-8035-af74d7e96be0&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare2.0.0-Desktop-ZH + +![MagicShare2.0.0-Web-ZH](https://lively-brook-dc1.notion.site/image/attachment%3Abdaae023-be5d-4c78-970a-49e835b5f90b%3AMagicShare2.0.0-Web-ZH.png?table=block&id=c391b6ab-fc8c-4657-8f8d-bc718423e298&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare2.0.0-Web-ZH + +### MagicShare1.0.0 + +![MagicShare1.0.0-Desktop](https://lively-brook-dc1.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fcc029b9b-e911-4cd4-b9c7-4b0853fa6d04%2FMagicShare1.0.0-Desktop.png?table=block&id=17fe64bd-e40f-8066-8c2d-f5f8b1ab0a23&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) MagicShare1.0.0-Desktop -![MagicShare1.0.0-Web](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fa457bfec-f826-4936-8721-75fc46632fc2%2FMagicShare1.0.0-Web.png?table=block&id=17fe64bd-e40f-80d7-9c7c-fca929d6904d&t=17fe64bd-e40f-80d7-9c7c-fca929d6904d&width=707&cache=v2) +![MagicShare1.0.0-Web](https://lively-brook-dc1.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fa457bfec-f826-4936-8721-75fc46632fc2%2FMagicShare1.0.0-Web.png?table=block&id=17fe64bd-e40f-80d7-9c7c-fca929d6904d&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) MagicShare1.0.0-Web diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..caaef60 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,152 @@ +# **MagicShare(English)** + +[**中文说明**](./README.md) + +--- + +Website: + +[MagicShare_EN | ZZHow](https://www.zzhow.com/en/MagicShareEN) + +Source Code: + +https://github.com/ZZHow1024/MagicShare + +Releases: + +https://github.com/ZZHow1024/MagicShare/releases + +--- + +## What is it? + +MagicShare is a cross-platform intranet file sharing tool. + +--- + +## Technical route + +![TechnicalRoute.png](./TechnicalRoute.png) + +- programming language: **Java** and **JavaScript** + +--- + +## **License** + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](https://github.com/ZZHow1024/MagicShare/blob/main/LICENSE) file for details. + +--- + +## User License Agreement + +**Please read carefully before using this software:** + +- Legal use: This software is limited to legal file sharing. It is strictly forbidden to share any files that infringe copyright, involve pornography, violence, fraud, illegal or other harmful content. +- Personal responsibility: You are fully responsible for the legality of the shared content. Please make sure that you have the legal authorization to share the file. +- Risk warning: This software cannot guarantee the security of the shared files. Please check the security of the files yourself. +- Disclaimer: The software author is not responsible for any direct or indirect losses caused by the use of this software. + +The use of MagicShare requires agreement and compliance with the above. + +--- + +## **Instructions for use** + +Download address: + +https://github.com/ZZHow1024/MagicShare/releases + +### Desktop Client + +- Determine the operating system you are using. + - Linux: + - Select the .deb installation package (Debian, Ubuntu) / .rpm (Red Hat, Fedora, SUSE) installation package. + - macOS: + - Determine the chip of the Mac you are using (Apple Silicon / Intel). + - Select the .dmg disk image / .pkg installation package. + - Windows: + - Select the .zip compressed package / .exe installation package / .msi installation package. + - General: + - Select the .jar package (the computer needs to have JRE configured) +- Download the corresponding file. +- Linux and macOS need to be installed before running. Windows can directly run the .exe executable program in the .zip compressed package or select the .exe installation package and .msi installation package to perform the installation operation. The .jar package can be directly run through the `java -jar` command. +- Launch MagicShare and read the instructions on the startup page carefully. You can continue to use it after agreeing to the "User License Agreement". +- You can select the language at the bottom right of the main interface of MagicShare. + - Currently supports Chinese (Simplified / Traditional) and English. +- Choose whether to enable the connection password + - If you want to enable the connection password, you need to check "Enable password" and customize a 3-10-digit password. + - If you do not want to enable the connection password, you need to uncheck "Enable password". +- After customizing the port, click the "Start Service" button + - If the prompt "Start Success" is displayed, it means that the service is started normally and the "Share URL" can be provided to the recipient. + - If the prompt "Port is occupied" is displayed, try to change the port number. + - If the prompt "Wrong port number", please check whether the customized port number is an integer between 1 and 65535. +- Add the files to be shared to the sharing list + - Method 1: Drag the file/folder to be shared to the upper half of the software main interface. + - Method 2: Click the "Select folder" button to select the folder to be shared. + - Method 3: Enter the path of the file/folder to be shared in the "Shared file/folder" text input box and press the "Enter" key. +- You can check the current number of connections in the upper right corner of the software. +- Press the "Stop Service" button to terminate sharing immediately. +- Press the "Clear sharing list" button to clear the share list immediately. + +### Web client + +- Open the browser and visit "Share URL". +- Launch MagicShare and read the instructions on the startup page carefully. Agree to the "User License Agreement" to continue using it. +- You can select the language in the lower right corner of the MagicShare main interface. + - Currently supports Chinese (Simplified/Traditional) and English. +- Download files + - Click "Quick download" to use the browser downloader to quickly download files via HTTP protocol. + - Click "Encrypted download" to use MagicShare encrypted downloader to download files via WebSocket protocol and use RSA+AES hybrid encryption. It does not support simultaneous encrypted download of multiple files. +- Click the "Decryption download progress" button, and the encrypted download progress drawer will pop up at the bottom of the page. + +--- + +## **Dependencies** + +This project requires the following libraries: + +- [**Vue.js**](https://github.com/vuejs) and supporting components: used to build Web frontend programs. +- [**Spring Boot**](https://github.com/spring-projects/spring-boot) and supporting components: used to build Web backend programs. +- [**OpenJFX**](https://openjfx.io/): JavaFX library for building graphical user interfaces. + +--- + +## **Functional introduction of each version** + +- MagicShare1.0.0 + - Start service on custom port. + - Find files by folder/file path and generate a list. + - Download files from Web page. + - Support fast file download via HTTP protocol + - Support downloading files via WebSocket protocol and using RSA+AES hybrid encryption +- MagicShare2.0.0 + - Display the current number of connections. + - Customize the connection password. + - Support multiple languages. + - Chinese (Simplified/Traditional) + - English + +--- + +## **Main interface of each version** + +### MagicShare2.0.0 + +![MagicShare2.0.0-Desktop-EN](https://lively-brook-dc1.notion.site/image/attachment%3Af185d39b-6a7c-4334-b51a-6821c72586c5%3AMagicShare2.0.0-Desktop-EN.png?table=block&id=760a4fa4-e27f-49a6-afd8-9fa26d5bf88b&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare2.0.0-Desktop-EN + +![MagicShare2.0.0-Web-EN](https://lively-brook-dc1.notion.site/image/attachment%3A7f1c2b9b-6013-4bf7-bab7-4ea71051fcb0%3AMagicShare2.0.0-Web-EN.png?table=block&id=b25b96d5-1040-4164-abd0-45f0c69b6c7a&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare2.0.0-Web-EN + +### MagicShare1.0.0 + +![MagicShare1.0.0-Desktop](https://lively-brook-dc1.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fcc029b9b-e911-4cd4-b9c7-4b0853fa6d04%2FMagicShare1.0.0-Desktop.png?table=block&id=194e64bd-e40f-8068-a52e-efc6ba94c62e&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare1.0.0-Desktop + +![MagicShare1.0.0-Web](https://lively-brook-dc1.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F4b165318-6383-451c-8845-110b786c9f0a%2Fa457bfec-f826-4936-8721-75fc46632fc2%2FMagicShare1.0.0-Web.png?table=block&id=194e64bd-e40f-80d1-957a-c58c4ea64b02&spaceId=4b165318-6383-451c-8845-110b786c9f0a&width=1420&userId=&cache=v2) + +MagicShare1.0.0-Web diff --git a/backend/pom.xml b/backend/pom.xml index 46882e4..868ad58 100755 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -10,7 +10,7 @@ com.zzhow MagicShare - 1.0.0 + 2.0.0 MagicShare MagicShare diff --git a/backend/src/main/java/com/zzhow/magicshare/config/WebSocketConfig.java b/backend/src/main/java/com/zzhow/magicshare/config/WebSocketConfig.java index 4e9ac33..ca82df6 100644 --- a/backend/src/main/java/com/zzhow/magicshare/config/WebSocketConfig.java +++ b/backend/src/main/java/com/zzhow/magicshare/config/WebSocketConfig.java @@ -1,6 +1,9 @@ package com.zzhow.magicshare.config; +import com.zzhow.magicshare.service.FileService; import com.zzhow.magicshare.websocket.FileWebSocketHandler; +import com.zzhow.magicshare.websocket.UserWebSocketHandler; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; @@ -13,9 +16,13 @@ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { + @Autowired + private FileService fileService; + @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注册 WebSocket 端点并指定处理器 registry.addHandler(new FileWebSocketHandler(), "/ws/download").setAllowedOrigins("*"); + registry.addHandler(new UserWebSocketHandler(fileService), "/ws/connect").setAllowedOrigins("*"); } } \ No newline at end of file diff --git a/backend/src/main/java/com/zzhow/magicshare/controller/DownloadController.java b/backend/src/main/java/com/zzhow/magicshare/controller/DownloadController.java index 34ea671..e1bd9b0 100644 --- a/backend/src/main/java/com/zzhow/magicshare/controller/DownloadController.java +++ b/backend/src/main/java/com/zzhow/magicshare/controller/DownloadController.java @@ -2,6 +2,8 @@ import com.zzhow.magicshare.pojo.entity.FileDetail; import com.zzhow.magicshare.repository.FileRepository; +import com.zzhow.magicshare.repository.UserRepository; +import com.zzhow.magicshare.util.CryptoUtil; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; @@ -12,6 +14,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.Base64; import java.util.List; /** @@ -21,17 +24,34 @@ @RestController() @RequestMapping("/api/download") public class DownloadController { + private final CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + @GetMapping("/{fileId}") - public ResponseEntity downloadFile(String shareId, @PathVariable String fileId) { - if (shareId == null || fileId == null) { - return ResponseEntity.badRequest().build(); - } + public ResponseEntity downloadFile(String token, String shareId, @PathVariable String fileId) { + try { + if (token == null || shareId == null || fileId == null) { + return ResponseEntity.badRequest().build(); + } - if (!shareId.equals(FileRepository.getUuid())) { - return ResponseEntity.notFound().build(); - } + fileId = fileId.replace("-", "/").replace("_", "+"); + token = token.replace(" ", "+"); + shareId = shareId.replace(" ", "+"); + + // AES 解密数据 + String[] split = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(token))).split("#"); + + // 验证 sessionId#downloadId + if (!UserRepository.verifyDownloadId(split[0], split[1])) + return ResponseEntity.notFound().build(); + + // 解密数据 + shareId = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(shareId))); + fileId = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(fileId))); + + if (!shareId.equals(FileRepository.getUuid())) { + return ResponseEntity.notFound().build(); + } - try { // 构建文件路径 List files = FileRepository.getFiles(); int index = files.indexOf(new FileDetail(fileId)); diff --git a/backend/src/main/java/com/zzhow/magicshare/controller/FileController.java b/backend/src/main/java/com/zzhow/magicshare/controller/FileController.java deleted file mode 100644 index 5a848ef..0000000 --- a/backend/src/main/java/com/zzhow/magicshare/controller/FileController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zzhow.magicshare.controller; - -import com.zzhow.magicshare.pojo.dto.CryptoDTO; -import com.zzhow.magicshare.pojo.vo.CryptoVO; -import com.zzhow.magicshare.result.Result; -import com.zzhow.magicshare.service.FileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -/** - * @author ZZHow - * @date 2025/01/14 - */ -@RestController() -@RequestMapping("/api/file") -public class FileController { - @Autowired - private FileService fileService; - - @PostMapping(path = "/list") - public Result fileList(@RequestBody CryptoDTO cryptoDTO) { - return Result.success(fileService.getFileList(cryptoDTO.getKey())); - } - - @GetMapping(path = "/check") - public Result checkCurrentShare(@RequestParam String shareId) { - return Result.success(fileService.checkCurrentShare(shareId)); - } -} diff --git a/backend/src/main/java/com/zzhow/magicshare/pojo/dto/CryptoDTO.java b/backend/src/main/java/com/zzhow/magicshare/pojo/dto/CryptoDTO.java deleted file mode 100644 index c8eedbf..0000000 --- a/backend/src/main/java/com/zzhow/magicshare/pojo/dto/CryptoDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zzhow.magicshare.pojo.dto; - -/** - * @author ZZHow - * @date 2025/01/15 - */ -public class CryptoDTO { - private String key; - - public CryptoDTO() { - } - - public CryptoDTO(String key) { - this.key = key; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } -} diff --git a/backend/src/main/java/com/zzhow/magicshare/pojo/entity/User.java b/backend/src/main/java/com/zzhow/magicshare/pojo/entity/User.java new file mode 100644 index 0000000..b6aac93 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/pojo/entity/User.java @@ -0,0 +1,30 @@ +package com.zzhow.magicshare.pojo.entity; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author ZZHow + * @date 2025/02/04 + */ +public class User { + private WebSocketSession session; + private final Map downloadId = new HashMap<>(); + + public User() { + } + + public User(WebSocketSession session) { + this.session = session; + } + + public WebSocketSession getSession() { + return session; + } + + public Map getDownloadIdList() { + return downloadId; + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/pojo/vo/CryptoVO.java b/backend/src/main/java/com/zzhow/magicshare/pojo/vo/CryptoVO.java deleted file mode 100644 index 35b8e5d..0000000 --- a/backend/src/main/java/com/zzhow/magicshare/pojo/vo/CryptoVO.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.zzhow.magicshare.pojo.vo; - -/** - * @author ZZHow - * @date 2025/01/15 - */ -public class CryptoVO { - private String key; - private String iv; - private String data; - - public CryptoVO() { - } - - public CryptoVO(String key, String iv, String data) { - this.key = key; - this.iv = iv; - this.data = data; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getIv() { - return iv; - } - - public void setIv(String iv) { - this.iv = iv; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/AesKeyRepository.java b/backend/src/main/java/com/zzhow/magicshare/repository/AesKeyRepository.java deleted file mode 100644 index dcb9ae4..0000000 --- a/backend/src/main/java/com/zzhow/magicshare/repository/AesKeyRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.zzhow.magicshare.repository; - -import com.zzhow.magicshare.pojo.entity.AesCrypto; - -import javax.crypto.SecretKey; -import java.util.HashMap; -import java.util.Map; - -/** - * @author ZZHow - * @date 2025/01/16 - */ -public class AesKeyRepository { - private static final Map keys = new HashMap<>(); - - private AesKeyRepository() { - } - - public static void clear() { - keys.clear(); - } - - public static void set(String session, AesCrypto aesCrypto) { - keys.put(session, aesCrypto); - } - - public static void set(String session, SecretKey key, byte[] iv) { - keys.put(session, new AesCrypto(key, iv)); - } - - public static void delete(String session) { - keys.remove(session); - } - - public static AesCrypto get(String session) { - return keys.get(session); - } -} diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/ConnectionCountBinding.java b/backend/src/main/java/com/zzhow/magicshare/repository/ConnectionCountBinding.java new file mode 100644 index 0000000..ac9935a --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/repository/ConnectionCountBinding.java @@ -0,0 +1,28 @@ +package com.zzhow.magicshare.repository; + +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +/** + * @author ZZHow + * @date 2025/2/1 + */ +public class ConnectionCountBinding { + private static final StringProperty countProperty = new SimpleStringProperty("0"); + + public static String getCount() { + return countProperty.get(); + } + + public static void setCount(String value) { + // 在 JavaFX 应用线程上执行 + Platform.runLater(() -> { + countProperty.set(value); + }); + } + + public static StringProperty countProperty() { + return countProperty; + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/DownloadUserRepository.java b/backend/src/main/java/com/zzhow/magicshare/repository/DownloadUserRepository.java new file mode 100644 index 0000000..d6689c3 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/repository/DownloadUserRepository.java @@ -0,0 +1,33 @@ +package com.zzhow.magicshare.repository; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author ZZHow + * @date 2025/2/8 + */ +public class DownloadUserRepository { + private static final DownloadUserRepository instance = new DownloadUserRepository(); + private final List users = new ArrayList<>(); + + private DownloadUserRepository() { + + } + + public void addUser(String sessionId) { + users.add(sessionId); + } + + public void removeUser(String sessionId) { + users.remove(sessionId); + } + + public boolean containsUser(String sessionId) { + return users.contains(sessionId); + } + + public static DownloadUserRepository getInstance() { + return instance; + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/FileRepository.java b/backend/src/main/java/com/zzhow/magicshare/repository/FileRepository.java index 836127b..40f49ea 100644 --- a/backend/src/main/java/com/zzhow/magicshare/repository/FileRepository.java +++ b/backend/src/main/java/com/zzhow/magicshare/repository/FileRepository.java @@ -29,6 +29,7 @@ public static void clearFiles() { uuid = UUID.randomUUID().toString(); basePath = ""; files.clear(); + UserRepository.sendListToAll(); } public static void addFile(FileDetail fileDetail) { @@ -46,6 +47,7 @@ public static String getUuid() { public static void setFiles(String bashPath, List files) { FileRepository.basePath = bashPath; FileRepository.files = files; + UserRepository.sendListToAll(); } public static void setBasePath(String basePath) { diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/LanguageRepository.java b/backend/src/main/java/com/zzhow/magicshare/repository/LanguageRepository.java new file mode 100644 index 0000000..58b4907 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/repository/LanguageRepository.java @@ -0,0 +1,22 @@ +package com.zzhow.magicshare.repository; + +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * @author ZZHow + * @date 2025/1/30 + */ +public class LanguageRepository { + public static ResourceBundle bundle = ResourceBundle.getBundle("MessagesBundle", Locale.of("zh", "HANS")); + private static String language = "zh_HANS"; + + public static String getLanguage() { + return language; + } + + public static void setLanguage(String language) { + LanguageRepository.language = language; + bundle = ResourceBundle.getBundle("MessagesBundle", Locale.of(language.split("_")[0], language.split("_")[1])); + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/repository/UserRepository.java b/backend/src/main/java/com/zzhow/magicshare/repository/UserRepository.java new file mode 100644 index 0000000..64fcea1 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/repository/UserRepository.java @@ -0,0 +1,152 @@ +package com.zzhow.magicshare.repository; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zzhow.magicshare.pojo.entity.AesCrypto; +import com.zzhow.magicshare.pojo.entity.User; +import com.zzhow.magicshare.pojo.vo.FileListVO; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import java.io.IOException; +import java.security.*; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author ZZHow + * @date 2025/1/30 + */ +public class UserRepository { + private static AesCrypto aesCrypto = null; + private static KeyPair keyPair = null; + private static String password = null; + private static final Map users = new HashMap<>(); + + static { + initialize(); + } + + public static void initialize() { + // 清空用户列表 + users.clear(); + + try { + // 生成随机 AES 密钥 + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(256); // 选择 AES 256 位密钥 + SecretKey aesKey = keyGenerator.generateKey(); + // 生成随机 IV(初始化向量) + byte[] iv = new byte[16]; // AES 块大小 128 位 + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(iv); // 填充随机数据到 IV 数组 + UserRepository.aesCrypto = new AesCrypto(aesKey, iv); + + // 生成 RSA 密钥对 + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + UserRepository.keyPair = keyPairGenerator.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static AesCrypto getAesCrypto() { + return aesCrypto; + } + + public static KeyPair getKeyPair() { + return keyPair; + } + + public static String getPassword() { + return password; + } + + public static void setPassword(String password) { + UserRepository.password = password; + } + + public static void clearPassword() { + UserRepository.password = null; + } + + public static void addUser(String sessionId, WebSocketSession session) { + users.put(sessionId, new User(session)); + ConnectionCountBinding.setCount(users.size() + ""); + } + + public static void removeUser(String sessionId) { + users.remove(sessionId); + ConnectionCountBinding.setCount(users.size() + ""); + } + + public static User getUser(String sessionId) { + return users.get(sessionId); + } + + public static boolean containsUser(String sessionId) { + return users.get(sessionId) != null; + } + + public static String generateDownloadId(String sessionId) { + String downloadId = UUID.randomUUID().toString(); + users.get(sessionId).getDownloadIdList().put(downloadId, true); + + return downloadId; + } + + public static boolean verifyDownloadId(String sessionId, String downloadId) { + if (users.get(sessionId) != null && users.get(sessionId).getDownloadIdList().containsKey(downloadId)) { + if (users.get(sessionId).getDownloadIdList().get(downloadId)) { + users.get(sessionId).getDownloadIdList().put(downloadId, false); + + // 延迟 10s 删除 downloadId + new java.util.concurrent.ScheduledThreadPoolExecutor(1) + .schedule(() -> { + if (!users.containsKey(sessionId)) return; + users.get(sessionId).getDownloadIdList().remove(downloadId); + }, 10, TimeUnit.SECONDS); + } + + return true; + } + + return false; + } + + public static void sendListToAll() { + Set strings = users.keySet(); + for (String sessionId : strings) { + User user = users.get(sessionId); + if (user != null && user.getSession().isOpen()) { + FileListVO fileListVO = new FileListVO(FileRepository.getUuid(), FileRepository.size(), FileRepository.getFiles()); + + try { + // 初始化 AES 加密器 + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec ivSpec = new IvParameterSpec(UserRepository.getAesCrypto().getIv()); + cipher.init(Cipher.ENCRYPT_MODE, UserRepository.getAesCrypto().getKey(), ivSpec); + + // 加密数据 + ObjectMapper objectMapper = new ObjectMapper(); + String jsonString = objectMapper.writeValueAsString(fileListVO); // 将对象序列化为 JSON 字符串 + byte[] encryptedData = cipher.doFinal(jsonString.getBytes()); + + // 发送加密数据 + user.getSession().sendMessage(new TextMessage("List#" + Base64.getEncoder().encodeToString(encryptedData))); + } catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | + InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | + InvalidKeyException e) { + try { + user.getSession().close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } + } + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/result/Result.java b/backend/src/main/java/com/zzhow/magicshare/result/Result.java deleted file mode 100644 index 386eb98..0000000 --- a/backend/src/main/java/com/zzhow/magicshare/result/Result.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.zzhow.magicshare.result; - -/** - * 统一的结果 - * - * @author ZZHow - * @date 2025/01/14 - */ -public class Result { - private Integer code; // 代码:1 成功,0 和其它数字为失败 - private String message; // 信息 - private T data; // 数据 - - public Result() { - } - - public Result(Integer code, String message, T data) { - this.code = code; - this.message = message; - this.data = data; - } - - public static Result success() { - return new Result<>(0, "success", null); - } - - public static Result success(String message, T object) { - return new Result<>(0, message, object); - } - - public static Result success(T object) { - return new Result<>(0, "success", object); - } - - public static Result error(String message) { - return new Result<>(1, message, null); - } - - public static Result error(Integer code, String message) { - return new Result<>(code, message, null); - } - - public static Result loginSuccessful(T object) { - return new Result<>(0, "Login successful", object); - } - - public static Result loginFailed(Integer code, String message) { - return new Result<>(code, message, null); - } - - public static Result unauthorized() { - return new Result<>(1, "Unauthorized", null); - } - - public static Result resourceNotFound() { - return new Result<>(1, "Resource Not Found", null); - } - - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public T getData() { - return data; - } - - public void setData(T data) { - this.data = data; - } -} diff --git a/backend/src/main/java/com/zzhow/magicshare/service/FileService.java b/backend/src/main/java/com/zzhow/magicshare/service/FileService.java index dc53121..364701b 100644 --- a/backend/src/main/java/com/zzhow/magicshare/service/FileService.java +++ b/backend/src/main/java/com/zzhow/magicshare/service/FileService.java @@ -1,8 +1,5 @@ package com.zzhow.magicshare.service; -import com.zzhow.magicshare.pojo.vo.CryptoVO; -import com.zzhow.magicshare.pojo.vo.FileListVO; - /** * @author ZZHow * @date 2025/01/14 @@ -11,10 +8,9 @@ public interface FileService { /** * 获得分享的文件列表 * - * @param publicKey 公钥 * @return 分享的文件列表(经过 AES 加密) */ - CryptoVO getFileList(String publicKey); + String getFileList(); /** * 检查是否为当前分享 diff --git a/backend/src/main/java/com/zzhow/magicshare/service/impl/FileServiceImpl.java b/backend/src/main/java/com/zzhow/magicshare/service/impl/FileServiceImpl.java index 2e61c2e..a70aaec 100644 --- a/backend/src/main/java/com/zzhow/magicshare/service/impl/FileServiceImpl.java +++ b/backend/src/main/java/com/zzhow/magicshare/service/impl/FileServiceImpl.java @@ -2,19 +2,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zzhow.magicshare.pojo.vo.CryptoVO; import com.zzhow.magicshare.pojo.vo.FileListVO; import com.zzhow.magicshare.repository.FileRepository; import com.zzhow.magicshare.service.FileService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.zzhow.magicshare.util.CryptoUtil; import org.springframework.stereotype.Service; import javax.crypto.*; -import javax.crypto.spec.IvParameterSpec; import java.security.*; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; import java.util.Base64; /** @@ -23,59 +18,27 @@ */ @Service public class FileServiceImpl implements FileService { - private static final Logger log = LoggerFactory.getLogger(FileServiceImpl.class); + private final CryptoUtil cryptoUtil = CryptoUtil.getInstance(); /** * 获得分享的文件列表 * - * @param publicKey 公钥 * @return 分享的文件列表 */ @Override - public CryptoVO getFileList(String publicKey) { + public String getFileList() { FileListVO fileListVO = new FileListVO(FileRepository.getUuid(), FileRepository.size(), FileRepository.getFiles()); try { - // Base64 解码 - publicKey = new String(Base64.getDecoder().decode(publicKey)); - // 去除 PEM 格式的头尾 - String publicKeyContent = publicKey.replaceAll("-----\\w+ PUBLIC KEY-----", "").replaceAll("\\s+", ""); - byte[] keyBytes = Base64.getDecoder().decode(publicKeyContent); - - // 加载公钥 - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey clientPublicKey = keyFactory.generatePublic(spec); - - // 生成随机 AES 密钥 - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(256); // 选择 AES 256 位密钥 - SecretKey aesKey = keyGenerator.generateKey(); - - // 生成随机 IV(初始化向量) - byte[] iv = new byte[16]; // AES 块大小 128 位 - SecureRandom secureRandom = new SecureRandom(); - secureRandom.nextBytes(iv); // 填充随机数据到 iv 数组 - - // 使用 RSA 加密 AES 密钥 - Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); - rsaCipher.init(Cipher.ENCRYPT_MODE, clientPublicKey); - byte[] encryptedAesKey = rsaCipher.doFinal(aesKey.getEncoded()); - - // 初始化 AES 加密器 - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.ENCRYPT_MODE, aesKey, ivSpec); - - // 加密数据 + // AES 加密数据 ObjectMapper objectMapper = new ObjectMapper(); String jsonString = objectMapper.writeValueAsString(fileListVO); // 将对象序列化为 JSON 字符串 - byte[] encryptedData = cipher.doFinal(jsonString.getBytes()); + byte[] encryptedData = cryptoUtil.encryptAes(jsonString); - // 返回加密的 AES 密钥、IV 和加密数据 - return new CryptoVO(Base64.getEncoder().encodeToString(encryptedAesKey), Base64.getEncoder().encodeToString(iv), Base64.getEncoder().encodeToString(encryptedData)); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | - InvalidKeySpecException | BadPaddingException | InvalidKeyException | JsonProcessingException | + // 返回加密数据 + return Base64.getEncoder().encodeToString(encryptedData); + } catch (IllegalBlockSizeException | + BadPaddingException | InvalidKeyException | JsonProcessingException | InvalidAlgorithmParameterException e) { throw new RuntimeException(e); } diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/controller/AboutController.java b/backend/src/main/java/com/zzhow/magicshare/ui/controller/AboutController.java new file mode 100644 index 0000000..b4cd48c --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/ui/controller/AboutController.java @@ -0,0 +1,40 @@ +package com.zzhow.magicshare.ui.controller; + +import com.zzhow.magicshare.repository.LanguageRepository; +import javafx.fxml.FXML; +import javafx.scene.control.Label; + +import java.util.ResourceBundle; + +/** + * @author ZZHow + * @date 2025/2/8 + */ +public class AboutController { + @FXML + private Label label1; + @FXML + private Label label2; + @FXML + private Label label3; + @FXML + private Label label4; + @FXML + private Label label5; + + @FXML + private void initialize() { + switchLanguage(); + } + + private void switchLanguage() { + ResourceBundle bundle = LanguageRepository.bundle; + + if (LanguageRepository.getLanguage().contains("zh")) + label2.setText(bundle.getString("magicShare") + " 2.0.0"); + else + label2.setVisible(false); + label3.setText(bundle.getString("features")); + label4.setText(bundle.getString("featuresContent")); + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/controller/MainController.java b/backend/src/main/java/com/zzhow/magicshare/ui/controller/MainController.java index f1a164b..dee3d59 100644 --- a/backend/src/main/java/com/zzhow/magicshare/ui/controller/MainController.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/controller/MainController.java @@ -1,7 +1,10 @@ package com.zzhow.magicshare.ui.controller; +import com.zzhow.magicshare.repository.ConnectionCountBinding; import com.zzhow.magicshare.repository.FileRepository; import com.zzhow.magicshare.pojo.entity.FileDetail; +import com.zzhow.magicshare.repository.LanguageRepository; +import com.zzhow.magicshare.repository.UserRepository; import com.zzhow.magicshare.ui.service.ShareService; import com.zzhow.magicshare.ui.service.impl.ShareServiceImpl; import com.zzhow.magicshare.ui.window.AboutWindow; @@ -9,6 +12,7 @@ import com.zzhow.magicshare.util.InternetUtil; import com.zzhow.magicshare.util.MessageBox; import javafx.beans.property.SimpleStringProperty; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.input.DragEvent; @@ -16,10 +20,11 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; import javafx.stage.DirectoryChooser; -import javafx.stage.FileChooser; import java.io.File; import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; /** * @author ZZHow @@ -39,15 +44,33 @@ public class MainController { @FXML private Label label6; @FXML + private Label label7; + @FXML + private Label label8; + @FXML + private Label label9; + @FXML + private Label label10; + @FXML private TextField textField1; @FXML private TextField textField2; @FXML + private TextField textField3; + @FXML private Button button1; @FXML private Button button2; @FXML + private Button button3; + @FXML + private Button button4; + @FXML + private CheckBox checkBox1; + @FXML private TableView tableView1; + @FXML + private ChoiceBox languageSelector; private final ShareService shareService = new ShareServiceImpl(); @@ -55,19 +78,23 @@ public class MainController { @FXML private void initialize() { + // 显示内网 IPv4 地址 label2.setText(InternetUtil.getLocalIpAddress()); + // 连接数数据绑定 + label10.textProperty().bind(ConnectionCountBinding.countProperty()); + // 创建列 - TableColumn fileNameCol = new TableColumn<>("文件名"); + TableColumn fileNameCol = new TableColumn<>("File name"); fileNameCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName())); - TableColumn fileTypeCol = new TableColumn<>("类型"); + TableColumn fileTypeCol = new TableColumn<>("Type"); fileTypeCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getType())); - TableColumn fileSizeCol = new TableColumn<>("大小(KB)"); + TableColumn fileSizeCol = new TableColumn<>("Size(KB)"); fileSizeCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getSize() + "")); - TableColumn filePathCol = new TableColumn<>("相对路径"); + TableColumn filePathCol = new TableColumn<>("Relative path"); filePathCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getPath())); tableView1.getColumns().addAll(fileNameCol, fileTypeCol, fileSizeCol, filePathCol); - tableView1.setPlaceholder(new Label("分享列表为空")); + tableView1.setPlaceholder(new Label("Share list is empty")); // 设置列的宽度比例 tableView1.widthProperty().addListener((obs, oldWidth, newWidth) -> { @@ -77,6 +104,29 @@ private void initialize() { fileSizeCol.setPrefWidth(totalWidth * 0.13); filePathCol.setPrefWidth(totalWidth * 0.50); }); + + languageSelector.getItems().addAll("简体中文", "繁體中文", "English"); + String language = Locale.getDefault().toLanguageTag(); + + if (language.contains("zh")) { + if (language.contains("CN") || language.contains("cn")) + language = "zh_HANS"; + else if (language.contains("HANS") || language.contains("Hans")) + language = "zh_HANS"; + else + language = "zh_HANT"; + } else { + language = "en_US"; + } + + language = switch (language) { + case "zh_HANS" -> "简体中文"; + case "zh_HANT" -> "繁體中文"; + case "en_US" -> "English"; + default -> "简体中文"; + }; + languageSelector.setValue(language); + switchLanguage(); } @FXML @@ -88,33 +138,44 @@ private void onStartOrStopServiceKeyDown(KeyEvent keyEvent) { @FXML private void onStartOrStopServiceClicked() { if (serviceIsStarted) { + UserRepository.initialize(); textField1.setDisable(false); - label1.setText("内网IPv4地址:"); + if (checkBox1.isSelected()) + textField3.setDisable(false); + checkBox1.setDisable(false); + label1.setText(LanguageRepository.bundle.getString("label1")); label2.setText(InternetUtil.getLocalIpAddress()); - button1.setText("启动服务"); + button1.setText(LanguageRepository.bundle.getString("button1")); shareService.stopService(); - MessageBox.success("停止成功", "MagicShare 服务停止成功"); + MessageBox.success(LanguageRepository.bundle.getString("stopSuccess"), LanguageRepository.bundle.getString("stopSuccessContent")); serviceIsStarted = false; return; } - - byte i = shareService.startService(textField1.getText()); + byte i = shareService.startService(textField1.getText(), textField3.getText(), checkBox1.isSelected()); switch (i) { case 0 -> { textField1.setDisable(true); - label1.setText("分享URL:"); + textField3.setDisable(true); + checkBox1.setDisable(true); + label1.setText(LanguageRepository.bundle.getString("shareUrl")); // 分享URL: label2.setText("http://" + InternetUtil.getLocalIpAddress() + ":" + textField1.getText()); - MessageBox.success("启动成功", "MagicShare 服务启动成功"); - button1.setText("停止服务"); + MessageBox.success(LanguageRepository.bundle.getString("startupSuccess"), LanguageRepository.bundle.getString("startupSuccessContent")); + button1.setText(LanguageRepository.bundle.getString("stopService")); // 停止服务 serviceIsStarted = true; } case 1 -> { - MessageBox.error("端口号错误", "端口号应为 1~65535 的整数"); + MessageBox.error(LanguageRepository.bundle.getString("wrongPortNumber"), LanguageRepository.bundle.getString("wrongPortNumberContent")); // 端口号错误 } case 2 -> { - MessageBox.error("端口被占用", "请尝试更换端口号"); + MessageBox.error(LanguageRepository.bundle.getString("portIsOccupied"), LanguageRepository.bundle.getString("portIsOccupiedContent")); // 端口被占用 + } + case 3 -> { + MessageBox.error(LanguageRepository.bundle.getString("wrongConnectionPassword"), LanguageRepository.bundle.getString("wrongConnectionPasswordContent1")); // 连接密码不能为空 + } + case 4 -> { + MessageBox.error(LanguageRepository.bundle.getString("wrongConnectionPassword"), LanguageRepository.bundle.getString("wrongConnectionPasswordContent2")); // 连接密码错误 } } } @@ -122,7 +183,7 @@ private void onStartOrStopServiceClicked() { @FXML private void onSelectFileClicked() { DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle("选择文件夹"); + directoryChooser.setTitle(LanguageRepository.bundle.getString("selectFolder")); // 选择文件夹 try { textField2.setText(directoryChooser.showDialog(MainWindow.getStage()).getAbsolutePath()); onSearchFileClicked(); @@ -178,4 +239,54 @@ private void onDragFile(DragEvent event) { private void onAboutClicked() { AboutWindow.open(); } + + @FXML + private void onEnablePasswordClicked() { + if (checkBox1.isSelected()) { + textField3.setDisable(false); + } else { + textField3.setText(""); + textField3.setDisable(true); + } + } + + @FXML + private void switchLanguage() { + String selectorValue = languageSelector.getValue(); + selectorValue = switch (selectorValue) { + case "简体中文" -> "zh_HANS"; + case "繁體中文" -> "zh_HANT"; + case "English" -> "en_US"; + default -> "zh_Hans"; + }; + + LanguageRepository.setLanguage(selectorValue); + + ResourceBundle bundle = LanguageRepository.bundle; + + ObservableList> columns = tableView1.getColumns(); + columns.get(0).setText(bundle.getString("fileName")); + columns.get(1).setText(bundle.getString("type")); + columns.get(2).setText(bundle.getString("size")); + columns.get(3).setText(bundle.getString("relativePath")); + tableView1.setPlaceholder(new Label(bundle.getString("shareListIsEmpty"))); + + if (serviceIsStarted) { + label1.setText(bundle.getString("shareUrl")); + button1.setText(bundle.getString("stopService")); + } else { + label1.setText(bundle.getString("label1")); + button1.setText(bundle.getString("button1")); + } + label3.setText(bundle.getString("label3")); + label4.setText(bundle.getString("label4")); + label5.setText(bundle.getString("label5")); + label7.setText(bundle.getString("label7")); + label8.setText(bundle.getString("label8")); + label9.setText(bundle.getString("label9")); + checkBox1.setText(bundle.getString("checkBox1")); + button2.setText(bundle.getString("button2")); + button3.setText(bundle.getString("button3")); + button4.setText(bundle.getString("button4")); + } } diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/controller/PromptController.java b/backend/src/main/java/com/zzhow/magicshare/ui/controller/PromptController.java index 9c8b35e..92a6d06 100644 --- a/backend/src/main/java/com/zzhow/magicshare/ui/controller/PromptController.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/controller/PromptController.java @@ -2,12 +2,52 @@ import com.zzhow.magicshare.ui.window.PromptWindow; import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; + +import java.util.Locale; +import java.util.ResourceBundle; /** * @author ZZHow * @date 2025/01/17 */ public class PromptController { + @FXML + private Label title; + @FXML + private Label content; + @FXML + private Button exit; + @FXML + private Button accept; + + @FXML + private void initialize() { + String language = Locale.getDefault().toLanguageTag(); + if (language.contains("zh")) { + if (language.contains("CN") || language.contains("cn")) + language = "zh_HANS"; + else if (language.contains("HANS") || language.contains("Hans")) + language = "zh_HANS"; + else + language = "zh_HANT"; + } else { + language = "en_US"; + } + + ResourceBundle bundle = ResourceBundle.getBundle("MessagesBundle", Locale.of(language.split("_")[0], language.split("_")[1])); + if (language.contains("en")) { + title.setVisible(false); + content.setLayoutY(65); + } else { + title.setText(bundle.getString("magicShare")); + } + content.setText(bundle.getString("promptContent")); + exit.setText(bundle.getString("exit")); + accept.setText(bundle.getString("accept")); + } + @FXML private void onAgreeClicked() { PromptWindow.close(); diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/service/ShareService.java b/backend/src/main/java/com/zzhow/magicshare/ui/service/ShareService.java index 17e0aa8..bf5b2a6 100644 --- a/backend/src/main/java/com/zzhow/magicshare/ui/service/ShareService.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/service/ShareService.java @@ -9,9 +9,9 @@ public interface ShareService { * 启动 MagicShare 服务 * * @param port 端口号 - * @return 0-启动成功;1-端口号错误;2-端口被占用 + * @return 0-启动成功;1-端口号错误;2-端口被占用;3-连接密码不能为空;4-连接密码错误 */ - byte startService(String port); + byte startService(String port, String password, boolean isEnablePassword); /** * 停止 MagicShare 服务 diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/service/impl/ShareServiceImpl.java b/backend/src/main/java/com/zzhow/magicshare/ui/service/impl/ShareServiceImpl.java index f64fff7..e3380d2 100644 --- a/backend/src/main/java/com/zzhow/magicshare/ui/service/impl/ShareServiceImpl.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/service/impl/ShareServiceImpl.java @@ -1,6 +1,8 @@ package com.zzhow.magicshare.ui.service.impl; +import com.zzhow.magicshare.pojo.entity.FileDetail; import com.zzhow.magicshare.repository.FileRepository; +import com.zzhow.magicshare.repository.UserRepository; import com.zzhow.magicshare.util.Application; import com.zzhow.magicshare.ui.service.ShareService; import com.zzhow.magicshare.util.FileUtil; @@ -8,6 +10,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; +import java.util.ArrayList; +import java.util.List; + /** * @author ZZHow * @date 2025/01/14 @@ -19,18 +24,23 @@ public class ShareServiceImpl implements ShareService { * 启动 MagicShare 服务 * * @param portStr 端口号 - * @return 0-启动成功;1-端口号错误;2-端口被占用 + * @return 0-启动成功;1-端口号错误;2-端口被占用;3-连接密码不能为空;4-连接密码错误 */ @Override - public byte startService(String portStr) { + public byte startService(String portStr, String password, boolean isEnablePassword) { try { int port = Integer.parseInt(portStr); if (port < 1 || port > 65535) return 1; if (InternetUtil.isPortInUse(port)) return 2; + if (isEnablePassword && (password == null || password.isEmpty())) + return 3; + if (isEnablePassword && (password.length() < 3 || password.length() > 10)) + return 4; else { applicationContext = Application.startSpringBoot("--server.port=" + port); + UserRepository.setPassword(isEnablePassword ? password : null); return 0; } @@ -54,8 +64,8 @@ public void stopService() { */ @Override public void searchFile(String path) { - FileRepository.clearFiles(); - FileRepository.setBasePath(path); - FileUtil.find(path, FileRepository.getFiles()); + List res = new ArrayList<>(); + FileUtil.find(path, res); + FileRepository.setFiles(path, res); } } diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/window/MainWindow.java b/backend/src/main/java/com/zzhow/magicshare/ui/window/MainWindow.java index ddc0579..0fa419f 100755 --- a/backend/src/main/java/com/zzhow/magicshare/ui/window/MainWindow.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/window/MainWindow.java @@ -20,7 +20,7 @@ public class MainWindow extends javafx.application.Application { @Override public void start(Stage stage) throws IOException { FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("main-window.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 600, 400); + Scene scene = new Scene(fxmlLoader.load(), 700, 500); stage.setTitle("MagicShare"); stage.setScene(scene); Image icon = new Image(Objects.requireNonNull(MagicShareApplication.class.getResourceAsStream("/image/icon.png"))); diff --git a/backend/src/main/java/com/zzhow/magicshare/ui/window/PromptWindow.java b/backend/src/main/java/com/zzhow/magicshare/ui/window/PromptWindow.java index 4f9dd54..7782dbd 100644 --- a/backend/src/main/java/com/zzhow/magicshare/ui/window/PromptWindow.java +++ b/backend/src/main/java/com/zzhow/magicshare/ui/window/PromptWindow.java @@ -20,7 +20,7 @@ public class PromptWindow extends javafx.application.Application { public void start(Stage stage) throws IOException { PromptWindow.stage = stage; FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("prompt-window.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 780, 450); + Scene scene = new Scene(fxmlLoader.load(), 780, 520); stage.setTitle("MagicShare"); stage.setScene(scene); Image icon = new Image(Objects.requireNonNull(MagicShareApplication.class.getResourceAsStream("/image/icon.png"))); diff --git a/backend/src/main/java/com/zzhow/magicshare/util/CryptoUtil.java b/backend/src/main/java/com/zzhow/magicshare/util/CryptoUtil.java new file mode 100644 index 0000000..a0d3c89 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/util/CryptoUtil.java @@ -0,0 +1,98 @@ +package com.zzhow.magicshare.util; + +import com.zzhow.magicshare.repository.UserRepository; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * @author ZZHow + * @date 2025/2/7 + */ +public class CryptoUtil { + private static final CryptoUtil instance; + private final IvParameterSpec ivSpec = new IvParameterSpec(UserRepository.getAesCrypto().getIv()); + private final Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + private final Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + + static { + try { + instance = new CryptoUtil(); + } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private CryptoUtil() throws NoSuchPaddingException, NoSuchAlgorithmException { + + } + + // RSA 解密 + public byte[] encryptRsa(String publicKey, byte[] cipherText) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + // 去除 PEM 格式的头尾 + String publicKeyContent = publicKey.replaceAll("-----\\w+ PUBLIC KEY-----", "").replaceAll("\\s+", ""); + byte[] keyBytes = Base64.getDecoder().decode(publicKeyContent); + + // 加载公钥 + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey clientPublicKey = keyFactory.generatePublic(spec); + + // 初始化 RSA 加密器 + rsaCipher.init(Cipher.ENCRYPT_MODE, clientPublicKey); + + return rsaCipher.doFinal(cipherText); + } + + public byte[] encryptRsa(String publicKey, String cipherText) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return encryptRsa(publicKey, cipherText.getBytes()); + } + + // AES 加密 + public byte[] encryptAes(byte[] plainText) throws InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + // 初始化 AES 加密器 + aesCipher.init(Cipher.ENCRYPT_MODE, UserRepository.getAesCrypto().getKey(), ivSpec); + + return aesCipher.doFinal(plainText); + } + + public byte[] encryptAes(String plainText) throws InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return encryptAes(plainText.getBytes()); + } + + // AES 解密 + public byte[] decryptAes(byte[] cipherText) throws InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + // 初始化 AES 解密器 + aesCipher.init(Cipher.DECRYPT_MODE, UserRepository.getAesCrypto().getKey(), ivSpec); + + return aesCipher.doFinal(cipherText); + } + + public byte[] decryptAes(String cipherText) throws InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return decryptAes(cipherText.getBytes()); + } + + // SHA256 哈希 + public byte[] sha256(byte[] plainText) { + try { + return MessageDigest.getInstance("SHA-256").digest(plainText); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public byte[] sha256(String plainText) { + return sha256(plainText.getBytes()); + } + + public static CryptoUtil getInstance() { + return instance; + } +} diff --git a/backend/src/main/java/com/zzhow/magicshare/util/FileUtil.java b/backend/src/main/java/com/zzhow/magicshare/util/FileUtil.java index 9559882..3e78853 100644 --- a/backend/src/main/java/com/zzhow/magicshare/util/FileUtil.java +++ b/backend/src/main/java/com/zzhow/magicshare/util/FileUtil.java @@ -48,7 +48,7 @@ public static void find(String path, List res) { if (fileName.lastIndexOf(".") != -1) fileType = fileName.substring(fileName.lastIndexOf(".") + 1); - FileDetail fileDetail = new FileDetail(generator.generateId() + "", fileName, fileType, Math.round(file.length() / 1024.0 * 10.0) / 10.0, file.getAbsolutePath().replace(FileRepository.getBasePath(), "")); + FileDetail fileDetail = new FileDetail(generator.generateId() + "", fileName, fileType, Math.round(file.length() / 1024.0 * 10.0) / 10.0, file.getAbsolutePath().replace(path, "")); res.add(fileDetail); } } diff --git a/backend/src/main/java/com/zzhow/magicshare/util/MessageBox.java b/backend/src/main/java/com/zzhow/magicshare/util/MessageBox.java index 935bc6b..bb1e3ea 100644 --- a/backend/src/main/java/com/zzhow/magicshare/util/MessageBox.java +++ b/backend/src/main/java/com/zzhow/magicshare/util/MessageBox.java @@ -1,6 +1,7 @@ package com.zzhow.magicshare.util; import com.zzhow.magicshare.MagicShareApplication; +import com.zzhow.magicshare.repository.LanguageRepository; import javafx.scene.control.Alert; import javafx.scene.image.Image; import javafx.stage.Stage; @@ -28,14 +29,14 @@ public static void alert(Alert.AlertType type, String title, String headerText, public static void error(String headerText, String contentText) { alert(Alert.AlertType.ERROR, - "错误", + LanguageRepository.bundle.getString("error"), headerText, contentText); } public static void success(String headerText, String contentText) { alert(Alert.AlertType.INFORMATION, - "成功", + LanguageRepository.bundle.getString("success"), headerText, contentText); } diff --git a/backend/src/main/java/com/zzhow/magicshare/websocket/FileWebSocketHandler.java b/backend/src/main/java/com/zzhow/magicshare/websocket/FileWebSocketHandler.java index 7b9e585..72fb95e 100644 --- a/backend/src/main/java/com/zzhow/magicshare/websocket/FileWebSocketHandler.java +++ b/backend/src/main/java/com/zzhow/magicshare/websocket/FileWebSocketHandler.java @@ -1,25 +1,20 @@ package com.zzhow.magicshare.websocket; -import com.zzhow.magicshare.pojo.entity.AesCrypto; import com.zzhow.magicshare.pojo.entity.FileDetail; -import com.zzhow.magicshare.repository.AesKeyRepository; +import com.zzhow.magicshare.repository.DownloadUserRepository; import com.zzhow.magicshare.repository.FileRepository; +import com.zzhow.magicshare.repository.UserRepository; +import com.zzhow.magicshare.util.CryptoUtil; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.Base64; import java.util.List; @@ -30,67 +25,66 @@ */ public class FileWebSocketHandler extends TextWebSocketHandler { private static final int CHUNK_SIZE = 8192; + private final CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + private final DownloadUserRepository downloadUserRepository = DownloadUserRepository.getInstance(); @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { - AesKeyRepository.delete(session.getId()); + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + downloadUserRepository.removeUser(session.getId()); } @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - // 消息格式:a,publicKey,fileId + // 消息格式:a,sessionId#downloadId,fileId if (message.getPayload().charAt(0) == 'a') { String[] split = message.getPayload().split(","); + // AES 解密数据 + String[] data = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(split[1]))).split("#"); + String fileId = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(split[2]))); + + // 验证 sessionId#downloadId + if (!UserRepository.verifyDownloadId(data[0], data[1])) { + session.sendMessage(new TextMessage("Not Acceptable")); + session.close(CloseStatus.NOT_ACCEPTABLE); + + return; + } + // 计算分块数量 List files = FileRepository.getFiles(); - int index = files.indexOf(new FileDetail(split[2])); + int index = files.indexOf(new FileDetail(fileId)); if (index == -1) { - session.sendMessage(new TextMessage("Not found")); + session.sendMessage(new TextMessage("Not Found")); session.close(CloseStatus.NOT_ACCEPTABLE); return; } int block = (int) Math.ceil(files.get(index).getSize() / (CHUNK_SIZE / 1024.0)); - // Base64 解码 - String key = new String(Base64.getDecoder().decode(split[1])); - - // 去除 PEM 格式的头尾 - key = key.replaceAll("-----\\w+ PUBLIC KEY-----", "").replaceAll("\\s+", ""); - byte[] keyBytes = Base64.getDecoder().decode(key); + // AES 加密数据 + byte[] encryptedData = cryptoUtil.encryptAes((block + "")); - // 加载公钥 - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey publicKey = keyFactory.generatePublic(spec); - - // 生成随机 AES 密钥 - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - keyGenerator.init(256); // 选择 AES 256 位密钥 - SecretKey aesKey = keyGenerator.generateKey(); - - // 生成随机 IV(初始化向量) - byte[] iv = new byte[16]; // AES 块大小 128 位 - SecureRandom secureRandom = new SecureRandom(); - secureRandom.nextBytes(iv); // 填充随机数据到 iv 数组 - - // 使用 RSA 加密 AES 密钥 - Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); - rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] encryptedAesKey = rsaCipher.doFinal(aesKey.getEncoded()); + downloadUserRepository.addUser(session.getId()); + session.sendMessage(new TextMessage("block#" + Base64.getEncoder().encodeToString(encryptedData))); + } else if ((message.getPayload().charAt(0) == 'b')) { // 消息格式:b,fileId + // 身份验证 + if (!downloadUserRepository.containsUser(session.getId())) { + session.sendMessage(new TextMessage("Not Acceptable")); + session.close(CloseStatus.NOT_ACCEPTABLE); - AesKeyRepository.set(session.getId(), aesKey, iv); - session.sendMessage(new TextMessage("key#" + Base64.getEncoder().encodeToString(encryptedAesKey) + ",iv#" + Base64.getEncoder().encodeToString(iv) + ",block#" + block)); + return; + } - // 消息格式:b,fileId - } else if ((message.getPayload().charAt(0) == 'b')) { String[] split = message.getPayload().split(","); + // AES 解密数据 + String fileId = new String(cryptoUtil.decryptAes(Base64.getDecoder().decode(split[1]))); + // 构建文件路径 List files = FileRepository.getFiles(); - int index = files.indexOf(new FileDetail(split[1])); + int index = files.indexOf(new FileDetail(fileId)); if (index == -1) { - session.sendMessage(new TextMessage("Not found")); + session.sendMessage(new TextMessage("Not Found")); session.close(CloseStatus.NOT_ACCEPTABLE); return; } @@ -99,9 +93,8 @@ public void handleTextMessage(WebSocketSession session, TextMessage message) thr // 初始化 AES 加密器 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - AesCrypto aesCrypto = AesKeyRepository.get(session.getId()); - IvParameterSpec ivSpec = new IvParameterSpec(aesCrypto.getIv()); - cipher.init(Cipher.ENCRYPT_MODE, aesCrypto.getKey(), ivSpec); + IvParameterSpec ivSpec = new IvParameterSpec(UserRepository.getAesCrypto().getIv()); + cipher.init(Cipher.ENCRYPT_MODE, UserRepository.getAesCrypto().getKey(), ivSpec); try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath))) { byte[] buffer = new byte[CHUNK_SIZE]; @@ -119,6 +112,7 @@ public void handleTextMessage(WebSocketSession session, TextMessage message) thr } // 发送结束 + downloadUserRepository.removeUser(session.getId()); session.sendMessage(new TextMessage("fin")); } catch (IOException e) { session.sendMessage(new TextMessage("Error: " + e.getMessage())); diff --git a/backend/src/main/java/com/zzhow/magicshare/websocket/UserWebSocketHandler.java b/backend/src/main/java/com/zzhow/magicshare/websocket/UserWebSocketHandler.java new file mode 100644 index 0000000..4af0236 --- /dev/null +++ b/backend/src/main/java/com/zzhow/magicshare/websocket/UserWebSocketHandler.java @@ -0,0 +1,155 @@ +package com.zzhow.magicshare.websocket; + +import com.zzhow.magicshare.repository.UserRepository; +import com.zzhow.magicshare.service.FileService; +import com.zzhow.magicshare.util.CryptoUtil; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import java.io.IOException; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Base64; + +/** + * @author ZZHow + * @date 2025/1/30 + */ +public class UserWebSocketHandler extends TextWebSocketHandler { + private final FileService fileService; + private final CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + + public UserWebSocketHandler(FileService fileService) { + this.fileService = fileService; + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + UserRepository.removeUser(session.getId()); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) { + if (message.getPayload().startsWith("ClientHello")) { + try { + // Base64 解码 + String publicKey = new String(Base64.getDecoder().decode(message.getPayload().split("#")[1])); + + // RSA 加密 + byte[] encryptedSessionId = cryptoUtil.encryptRsa(publicKey, session.getId()); + + if (UserRepository.getPassword() == null) { + UserRepository.addUser(session.getId(), session); + session.sendMessage(new TextMessage("Syn#202#" + Base64.getEncoder().encodeToString(encryptedSessionId))); + } else + session.sendMessage(new TextMessage("ServerHello#" + Base64.getEncoder().encodeToString(encryptedSessionId))); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | + IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) { + try { + session.close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } else if (message.getPayload().startsWith("Syn")) { + byte[] decode = Base64.getDecoder().decode(message.getPayload().split("#")[1]); + + try { + String data = session.getId() + UserRepository.getPassword(); + byte[] hashBytes = cryptoUtil.sha256(data); + + if (Arrays.equals(hashBytes, decode)) { + UserRepository.addUser(session.getId(), session); + session.sendMessage(new TextMessage("Syn#200")); + } else { + session.sendMessage(new TextMessage("Syn#401")); + } + } catch (IOException e) { + try { + session.close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } else if (message.getPayload().startsWith("Exc")) { + try { + if (!UserRepository.containsUser(session.getId())) { + try { + session.close(CloseStatus.NOT_ACCEPTABLE); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return; + } + + // Base64 解码 + String publicKey = new String(Base64.getDecoder().decode(message.getPayload().split("#")[1])); + + // 使用 RSA 加密 AES 密钥 + byte[] encryptedAesKey = cryptoUtil.encryptRsa(publicKey, UserRepository.getAesCrypto().getKey().getEncoded()); + + // 返回加密的 AES 密钥和 IV + session.sendMessage(new TextMessage("Exc#" + Base64.getEncoder().encodeToString(encryptedAesKey) + "#" + Base64.getEncoder().encodeToString(UserRepository.getAesCrypto().getIv()))); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | + IllegalBlockSizeException | BadPaddingException | IOException | InvalidKeyException e) { + try { + session.close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } else if (message.getPayload().startsWith("List")) { + if (!UserRepository.containsUser(session.getId())) { + try { + session.close(CloseStatus.NOT_ACCEPTABLE); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return; + } + + String[] split = message.getPayload().split("#"); + String shareId = ""; + if (split.length > 1) + shareId = new String(Base64.getDecoder().decode(split[1].getBytes())); + if (!fileService.checkCurrentShare(shareId)) + try { + session.sendMessage(new TextMessage("List#" + fileService.getFileList())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else if (message.getPayload().startsWith("Download")) { + if (!UserRepository.containsUser(session.getId())) { + try { + session.close(CloseStatus.NOT_ACCEPTABLE); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return; + } + + String downloadId = UserRepository.generateDownloadId(session.getId()); + try { + // AES 加密数据 + byte[] encryptedData = cryptoUtil.encryptAes(downloadId); + session.sendMessage(new TextMessage("Download#" + message.getPayload().split("#")[1] + "#" + Base64.getEncoder().encodeToString(encryptedData))); + } catch (InvalidKeyException | + InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | + IOException e) { + try { + session.close(CloseStatus.SERVER_ERROR); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } + } +} diff --git a/backend/src/main/resources/MessagesBundle_en_US.properties b/backend/src/main/resources/MessagesBundle_en_US.properties new file mode 100644 index 0000000..3fdb493 --- /dev/null +++ b/backend/src/main/resources/MessagesBundle_en_US.properties @@ -0,0 +1,46 @@ +magicShare=MagicShare + +promptContent=\u0050\u006c\u0065\u0061\u0073\u0065\u0020\u0072\u0065\u0061\u0064\u0020\u0063\u0061\u0072\u0065\u0066\u0075\u006c\u006c\u0079\u0020\u0062\u0065\u0066\u006f\u0072\u0065\u0020\u0075\u0073\u0069\u006e\u0067\u0020\u0074\u0068\u0069\u0073\u0020\u0073\u006f\u0066\u0074\u0077\u0061\u0072\u0065\u003a\u000a\u000a\u004c\u0065\u0067\u0061\u006c\u0020\u0075\u0073\u0065\u003a\u0020\u0054\u0068\u0069\u0073\u0020\u0073\u006f\u0066\u0074\u0077\u0061\u0072\u0065\u0020\u0069\u0073\u0020\u006c\u0069\u006d\u0069\u0074\u0065\u0064\u0020\u0074\u006f\u0020\u006c\u0065\u0067\u0061\u006c\u0020\u0066\u0069\u006c\u0065\u0020\u0073\u0068\u0061\u0072\u0069\u006e\u0067\u002e\u0020\u0049\u0074\u0020\u0069\u0073\u0020\u0073\u0074\u0072\u0069\u0063\u0074\u006c\u0079\u0020\u0066\u006f\u0072\u0062\u0069\u0064\u0064\u0065\u006e\u0020\u0074\u006f\u0020\u0073\u0068\u0061\u0072\u0065\u0020\u0061\u006e\u0079\u0020\u0066\u0069\u006c\u0065\u0073\u0020\u0074\u0068\u0061\u0074\u0020\u0069\u006e\u0066\u0072\u0069\u006e\u0067\u0065\u0020\u0063\u006f\u0070\u0079\u0072\u0069\u0067\u0068\u0074\u002c\u0020\u0069\u006e\u0076\u006f\u006c\u0076\u0065\u0020\u0070\u006f\u0072\u006e\u006f\u0067\u0072\u0061\u0070\u0068\u0079\u002c\u0020\u0076\u0069\u006f\u006c\u0065\u006e\u0063\u0065\u002c\u0020\u0066\u0072\u0061\u0075\u0064\u002c\u0020\u0069\u006c\u006c\u0065\u0067\u0061\u006c\u0020\u006f\u0072\u0020\u006f\u0074\u0068\u0065\u0072\u0020\u0068\u0061\u0072\u006d\u0066\u0075\u006c\u0020\u0063\u006f\u006e\u0074\u0065\u006e\u0074\u002e\u000a\u0050\u0065\u0072\u0073\u006f\u006e\u0061\u006c\u0020\u0072\u0065\u0073\u0070\u006f\u006e\u0073\u0069\u0062\u0069\u006c\u0069\u0074\u0079\u003a\u0020\u0059\u006f\u0075\u0020\u0061\u0072\u0065\u0020\u0066\u0075\u006c\u006c\u0079\u0020\u0072\u0065\u0073\u0070\u006f\u006e\u0073\u0069\u0062\u006c\u0065\u0020\u0066\u006f\u0072\u0020\u0074\u0068\u0065\u0020\u006c\u0065\u0067\u0061\u006c\u0069\u0074\u0079\u0020\u006f\u0066\u0020\u0074\u0068\u0065\u0020\u0073\u0068\u0061\u0072\u0065\u0064\u0020\u0063\u006f\u006e\u0074\u0065\u006e\u0074\u002e\u0020\u0050\u006c\u0065\u0061\u0073\u0065\u0020\u006d\u0061\u006b\u0065\u0020\u0073\u0075\u0072\u0065\u0020\u0074\u0068\u0061\u0074\u0020\u0079\u006f\u0075\u0020\u0068\u0061\u0076\u0065\u0020\u0074\u0068\u0065\u0020\u006c\u0065\u0067\u0061\u006c\u0020\u0061\u0075\u0074\u0068\u006f\u0072\u0069\u007a\u0061\u0074\u0069\u006f\u006e\u0020\u0074\u006f\u0020\u0073\u0068\u0061\u0072\u0065\u0020\u0074\u0068\u0065\u0020\u0066\u0069\u006c\u0065\u002e\u000a\u0052\u0069\u0073\u006b\u0020\u0077\u0061\u0072\u006e\u0069\u006e\u0067\u003a\u0020\u0054\u0068\u0069\u0073\u0020\u0073\u006f\u0066\u0074\u0077\u0061\u0072\u0065\u0020\u0063\u0061\u006e\u006e\u006f\u0074\u0020\u0067\u0075\u0061\u0072\u0061\u006e\u0074\u0065\u0065\u0020\u0074\u0068\u0065\u0020\u0073\u0065\u0063\u0075\u0072\u0069\u0074\u0079\u0020\u006f\u0066\u0020\u0074\u0068\u0065\u0020\u0073\u0068\u0061\u0072\u0065\u0064\u0020\u0066\u0069\u006c\u0065\u0073\u002e\u0020\u0050\u006c\u0065\u0061\u0073\u0065\u0020\u0063\u0068\u0065\u0063\u006b\u0020\u0074\u0068\u0065\u0020\u0073\u0065\u0063\u0075\u0072\u0069\u0074\u0079\u0020\u006f\u0066\u0020\u0074\u0068\u0065\u0020\u0066\u0069\u006c\u0065\u0073\u0020\u0079\u006f\u0075\u0072\u0073\u0065\u006c\u0066\u002e\u000a\u0044\u0069\u0073\u0063\u006c\u0061\u0069\u006d\u0065\u0072\u003a\u0020\u0054\u0068\u0065\u0020\u0073\u006f\u0066\u0074\u0077\u0061\u0072\u0065\u0020\u0061\u0075\u0074\u0068\u006f\u0072\u0020\u0069\u0073\u0020\u006e\u006f\u0074\u0020\u0072\u0065\u0073\u0070\u006f\u006e\u0073\u0069\u0062\u006c\u0065\u0020\u0066\u006f\u0072\u0020\u0061\u006e\u0079\u0020\u0064\u0069\u0072\u0065\u0063\u0074\u0020\u006f\u0072\u0020\u0069\u006e\u0064\u0069\u0072\u0065\u0063\u0074\u0020\u006c\u006f\u0073\u0073\u0065\u0073\u0020\u0063\u0061\u0075\u0073\u0065\u0064\u0020\u0062\u0079\u0020\u0074\u0068\u0065\u0020\u0075\u0073\u0065\u0020\u006f\u0066\u0020\u0074\u0068\u0069\u0073\u0020\u0073\u006f\u0066\u0074\u0077\u0061\u0072\u0065\u002e +exit=Exit +accept=Accept + +label1=Intranet IPv4 address: +label3=Port number: +label4=Shared file/folder: +label5=Number of shared files +label7=Drag the file/folder to be shared here to get the path +label8=Connection password: +label9=Current number of connections +checkBox1=Enable Password +button1=Start service +button2=Select folder +button3=Clear sharing list +button4=About +fileName=File name +type=Type +size=Size(KB) +relativePath=Relative path +shareListIsEmpty=Sharing list is empty + +shareUrl=Share URL: +stopService=Stop service + +selectFolder=Select Folder + +startupSuccess=Start Success +startupSuccessContent=MagicShare service started successfully +stopSuccess=Stop Success +stopSuccessContent=MagicShare service stopped successfully +wrongPortNumber=Wrong port number +wrongPortNumberContent=The port number should be an integer from 1 to 65535. +portIsOccupied=Port is occupied +portIsOccupiedContent=Please try changing the port number +wrongConnectionPassword=Invalid connection password +wrongConnectionPasswordContent1=The connection password cannot be empty +wrongConnectionPasswordContent2=The connection password should be 3-10 characters long + +success=Success +error=Error + +features=New Features: +featuresContent=\u0031\u002e\u0020\u0044\u0069\u0073\u0070\u006c\u0061\u0079\u0020\u0074\u0068\u0065\u0020\u0063\u0075\u0072\u0072\u0065\u006e\u0074\u0020\u006e\u0075\u006d\u0062\u0065\u0072\u0020\u006f\u0066\u0020\u0063\u006f\u006e\u006e\u0065\u0063\u0074\u0069\u006f\u006e\u0073\u002e\u000a\u0032\u002e\u0020\u0043\u0075\u0073\u0074\u006f\u006d\u0069\u007a\u0065\u0020\u0074\u0068\u0065\u0020\u0063\u006f\u006e\u006e\u0065\u0063\u0074\u0069\u006f\u006e\u0020\u0070\u0061\u0073\u0073\u0077\u006f\u0072\u0064\u002e\u000a\u0033\u002e\u0020\u0053\u0075\u0070\u0070\u006f\u0072\u0074\u0020\u006d\u0075\u006c\u0074\u0069\u0070\u006c\u0065\u0020\u006c\u0061\u006e\u0067\u0075\u0061\u0067\u0065\u0073\u002e\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u0043\u0068\u0069\u006e\u0065\u0073\u0065\u0020\u0028\u0053\u0069\u006d\u0070\u006c\u0069\u0066\u0069\u0065\u0064\u002f\u0054\u0072\u0061\u0064\u0069\u0074\u0069\u006f\u006e\u0061\u006c\u0029\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u0045\u006e\u0067\u006c\u0069\u0073\u0068 \ No newline at end of file diff --git a/backend/src/main/resources/MessagesBundle_zh_HANS.properties b/backend/src/main/resources/MessagesBundle_zh_HANS.properties new file mode 100644 index 0000000..be8bea1 --- /dev/null +++ b/backend/src/main/resources/MessagesBundle_zh_HANS.properties @@ -0,0 +1,45 @@ +magicShare=\u795e\u5947\u5206\u4eab +promptContent=\u4f7f\u7528\u672c\u8f6f\u4ef6\u524d\uff0c\u8bf7\u4ed4\u7ec6\u9605\u8bfb\uff1a\u000a\u000a\u5408\u6cd5\u4f7f\u7528\uff1a\u0020\u672c\u8f6f\u4ef6\u4ec5\u9650\u4e8e\u5408\u6cd5\u6587\u4ef6\u5206\u4eab\uff0c\u4e25\u7981\u5206\u4eab\u4efb\u4f55\u4fb5\u72af\u7248\u6743\u3001\u6d89\u53ca\u8272\u60c5\u3001\u66b4\u529b\u3001\u6b3a\u8bc8\u3001\u8fdd\u6cd5\u6216\u5176\u4ed6\u6709\u5bb3\u5185\u5bb9\u7684\u6587\u4ef6\u3002\u000a\u4e2a\u4eba\u8d23\u4efb\uff1a\u0020\u60a8\u5bf9\u5206\u4eab\u5185\u5bb9\u7684\u5408\u6cd5\u6027\u8d1f\u5168\u90e8\u8d23\u4efb\uff0c\u8bf7\u786e\u4fdd\u60a8\u62e5\u6709\u5206\u4eab\u6587\u4ef6\u7684\u5408\u6cd5\u6388\u6743\u3002\u000a\u98ce\u9669\u63d0\u793a\uff1a\u0020\u672c\u8f6f\u4ef6\u65e0\u6cd5\u4fdd\u8bc1\u6240\u5206\u4eab\u6587\u4ef6\u7684\u5b89\u5168\u6027\uff0c\u8bf7\u60a8\u81ea\u884c\u68c0\u67e5\u6587\u4ef6\u7684\u5b89\u5168\u6027\u3002\u000a\u514d\u8d23\u58f0\u660e\uff1a\u0020\u8f6f\u4ef6\u4f5c\u8005\u4e0d\u5bf9\u56e0\u4f7f\u7528\u672c\u8f6f\u4ef6\u9020\u6210\u7684\u4efb\u4f55\u76f4\u63a5\u6216\u95f4\u63a5\u635f\u5931\u627f\u62c5\u8d23\u4efb\u3002 +exit=\u9000\u51fa +accept=\u63a5\u53d7 + +label1=\u5185\u7f51\u0049\u0050\u0076\u0034\u5730\u5740\uff1a +label3=\u7aef\u53e3\u53f7\uff1a +label4=\u5206\u4eab\u7684\u6587\u4ef6\u002f\u6587\u4ef6\u5939\uff1a +label5=\u5206\u4eab\u7684\u6587\u4ef6\u4e2a\u6570 +label7=\u62d6\u62fd\u5f85\u5206\u4eab\u7684\u6587\u4ef6\u002f\u6587\u4ef6\u5939\u5230\u6b64\u5904\u4ee5\u83b7\u5f97\u8def\u5f84 +label8=\u8fde\u63a5\u5bc6\u7801\uff1a +label9=\u5f53\u524d\u8fde\u63a5\u6570 +checkBox1=\u542f\u7528\u5bc6\u7801 +button1=\u542f\u52a8\u670d\u52a1 +button2=\u9009\u62e9\u6587\u4ef6\u5939 +button3=\u6e05\u7a7a\u5206\u4eab\u5217\u8868 +button4=\u5173\u4e8e +fileName=\u6587\u4ef6\u540d +type=\u7c7b\u578b +size=\u5927\u5c0f(KB) +relativePath=\u76f8\u5bf9\u8def\u5f84 +shareListIsEmpty=\u5206\u4eab\u5217\u8868\u4e3a\u7a7a + +shareUrl=\u5206\u4eab\u0055\u0052\u004c\uff1a +stopService=\u505c\u6b62\u670d\u52a1 + +selectFolder=\u9078\u64c7\u8cc7\u6599\u593e + +startupSuccess=\u542f\u52a8\u6210\u529f +startupSuccessContent=\u004d\u0061\u0067\u0069\u0063\u0053\u0068\u0061\u0072\u0065\u0020\u670d\u52a1\u542f\u52a8\u6210\u529f +stopSuccess=\u505c\u6b62\u6210\u529f +stopSuccessContent=\u004d\u0061\u0067\u0069\u0063\u0053\u0068\u0061\u0072\u0065\u0020\u670d\u52a1\u505c\u6b62\u6210\u529f +wrongPortNumber=\u7aef\u53e3\u53f7\u9519\u8bef +wrongPortNumberContent=\u7aef\u53e3\u53f7\u5e94\u4e3a\u0020\u0031\uff5e\u0036\u0035\u0035\u0033\u0035\u0020\u7684\u6574\u6570 +portIsOccupied=\u7aef\u53e3\u88ab\u5360\u7528 +portIsOccupiedContent=\u8bf7\u5c1d\u8bd5\u66f4\u6362\u7aef\u53e3\u53f7 +wrongConnectionPassword=\u8fde\u63a5\u5bc6\u7801\u4e0d\u5408\u6cd5 +wrongConnectionPasswordContent1=\u8fde\u63a5\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a +wrongConnectionPasswordContent2=\u8fde\u63a5\u5bc6\u7801\u957f\u5ea6\u5e94\u4e3a\u0033\u002d\u0031\u0030\u4e2a\u5b57\u7b26 + +success=\u6210\u529f +error=\u9519\u8bef + +features=\u65b0\u7279\u6027\uff1a +featuresContent=\u0031\u002e\u0020\u5f53\u524d\u8fde\u63a5\u6570\u663e\u793a\u3002\u000a\u0032\u002e\u0020\u81ea\u5b9a\u4e49\u8fde\u63a5\u5bc6\u7801\u3002\u000a\u0033\u002e\u0020\u652f\u6301\u591a\u8bed\u8a00\u3002\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u4e2d\u6587\uff08\u7b80\u4f53\u002f\u7e41\u4f53\uff09\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u82f1\u6587 \ No newline at end of file diff --git a/backend/src/main/resources/MessagesBundle_zh_HANT.properties b/backend/src/main/resources/MessagesBundle_zh_HANT.properties new file mode 100644 index 0000000..d4e9b52 --- /dev/null +++ b/backend/src/main/resources/MessagesBundle_zh_HANT.properties @@ -0,0 +1,45 @@ +magicShare=\u795e\u5947\u5206\u4eab +promptContent=\u4f7f\u7528\u672c\u8edf\u9ad4\u524d\uff0c\u8acb\u4ed4\u7d30\u95b1\u8b80\uff1a\u000a\u000a\u5408\u6cd5\u4f7f\u7528\uff1a\u0020\u672c\u8edf\u9ad4\u50c5\u9650\u65bc\u5408\u6cd5\u6587\u4ef6\u5206\u4eab\uff0c\u56b4\u7981\u5206\u4eab\u4efb\u4f55\u4fb5\u72af\u7248\u6b0a\u3001\u6d89\u53ca\u8272\u60c5\u3001\u66b4\u529b\u3001\u8a50\u6b3a\u3001\u9055\u6cd5\u6216\u5176\u4ed6\u6709\u5bb3\u5167\u5bb9\u7684\u6587\u4ef6\u3002\u000a\u500b\u4eba\u8cac\u4efb\uff1a\u0020\u60a8\u5c0d\u5206\u4eab\u5167\u5bb9\u7684\u5408\u6cd5\u6027\u8ca0\u5168\u90e8\u8cac\u4efb\uff0c\u8acb\u78ba\u4fdd\u60a8\u64c1\u6709\u5206\u4eab\u6587\u4ef6\u7684\u5408\u6cd5\u6388\u6b0a\u3002\u000a\u98a8\u96aa\u63d0\u793a\uff1a\u0020\u672c\u8edf\u9ad4\u7121\u6cd5\u4fdd\u8b49\u6240\u5206\u4eab\u6587\u4ef6\u7684\u5b89\u5168\u6027\uff0c\u8acb\u60a8\u81ea\u884c\u6aa2\u67e5\u6587\u4ef6\u7684\u5b89\u5168\u6027\u3002\u000a\u514d\u8cac\u8072\u660e\uff1a\u0020\u8edf\u9ad4\u4f5c\u8005\u4e0d\u5c0d\u56e0\u4f7f\u7528\u672c\u8edf\u9ad4\u9020\u6210\u7684\u4efb\u4f55\u76f4\u63a5\u6216\u9593\u63a5\u640d\u5931\u8ca0\u8cac\u3002 +exit=\u9000\u51fa +accept=\u63a5\u53d7 + +label1=\u5167\u7db2\u0049\u0050\u0076\u0034\u4f4d\u5740\uff1a +label3=\u9023\u63a5\u57e0\u865f\u78bc\uff1a +label4=\u5206\u4eab\u7684\u6a94\u6848\u002f\u8cc7\u6599\u593e\uff1a +label5=\u5206\u4eab\u7684\u6a94\u6848\u500b\u6578 +label7=\u62d6\u66f3\u5f85\u5206\u4eab\u7684\u6a94\u6848\u002f\u8cc7\u6599\u593e\u5230\u6b64\u8655\u4ee5\u7372\u5f97\u8def\u5f91 +label8=\u9023\u7dda\u5bc6\u78bc\uff1a +label9=\u76ee\u524d\u9023\u7dda\u6578 +checkBox1=\u555f\u7528\u5bc6\u78bc +button1=\u555f\u52d5\u670d\u52d9 +button2=\u9078\u64c7\u8cc7\u6599\u593e +button3=\u6e05\u7a7a\u5206\u4eab\u6e05\u55ae +button4=\u95dc\u65bc +fileName=\u6a94\u540d +type=\u578b\u5225 +size=\u5927\u5c0f(KB) +relativePath=\u76f8\u5c0d\u8def\u5f91 +shareListIsEmpty=\u5206\u4eab\u6e05\u55ae\u70ba\u7a7a + +shareUrl=\u5206\u4eab\u0055\u0052\u004c\uff1a +stopService=\u505c\u6b62\u670d\u52d9 + +selectFolder=\u9009\u62e9\u6587\u4ef6\u5939 + +startupSuccess=\u555f\u52d5\u6210\u529f +startupSuccessContent=\u004d\u0061\u0067\u0069\u0063\u0053\u0068\u0061\u0072\u0065\u0020\u670d\u52d9\u555f\u52d5\u6210\u529f +stopSuccess=\u505c\u6b62\u6210\u529f +stopSuccessContent=\u004d\u0061\u0067\u0069\u0063\u0053\u0068\u0061\u0072\u0065\u0020\u670d\u52d9\u505c\u6b62\u6210\u529f +wrongPortNumber=\u9023\u63a5\u57e0\u865f\u78bc\u932f\u8aa4 +wrongPortNumberContent=\u9023\u63a5\u57e0\u865f\u78bc\u61c9\u70ba\u0020\u0031\uff5e\u0036\u0035\u0035\u0033\u0035\u0020\u7684\u6574\u6578 +portIsOccupied=\u9023\u63a5\u57e0\u88ab\u4f54\u7528 +portIsOccupiedContent=\u8acb\u5617\u8a66\u66f4\u63db\u9023\u63a5\u57e0\u865f\u78bc +wrongConnectionPassword=\u9023\u7dda\u5bc6\u78bc\u4e0d\u5408\u6cd5 +wrongConnectionPasswordContent1=\u9023\u7dda\u5bc6\u78bc\u4e0d\u80fd\u70ba\u7a7a +wrongConnectionPasswordContent2=\u9023\u63a5\u5bc6\u78bc\u9577\u5ea6\u61c9\u70ba\u0033\u002d\u0031\u0030\u500b\u5b57\u7b26 + +success=\u6210\u529f +error=\u932f\u8aa4 + +features=\u65b0\u7279\u6027\uff1a +featuresContent=\u0031\u002e\u0020\u76ee\u524d\u9023\u7dda\u6578\u986f\u793a\u3002\u000a\u0032\u002e\u0020\u81ea\u8a02\u9023\u7dda\u5bc6\u78bc\u3002\u000a\u0033\u002e\u0020\u652f\u63f4\u591a\u8a9e\u8a00\u3002\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u4e2d\u6587\uff08\u7c21\u9ad4\u002f\u7e41\u9ad4\uff09\u000a\u0020\u0020\u0020\u0020\u002d\u0020\u82f1\u6587 \ No newline at end of file diff --git a/backend/src/main/resources/com/zzhow/magicshare/ui/window/about-window.fxml b/backend/src/main/resources/com/zzhow/magicshare/ui/window/about-window.fxml index f7cb21f..32da361 100644 --- a/backend/src/main/resources/com/zzhow/magicshare/ui/window/about-window.fxml +++ b/backend/src/main/resources/com/zzhow/magicshare/ui/window/about-window.fxml @@ -4,29 +4,29 @@ - + -