From cd4f13190f09108bec774484d6a217f67db71c40 Mon Sep 17 00:00:00 2001 From: M0914 <2625053283@qq.com> Date: Thu, 11 Sep 2025 14:19:43 +0800 Subject: [PATCH] enjoy!!! --- .gitignore | 37 + Dockerfile | 12 + README.md | 51 + docker-compose.yml | 10 + pom.xml | 154 + .../help/QiuMoJetbrainsHelpApplication.java | 166 ++ .../help/context/AgentContextHolder.java | 241 ++ .../context/CertificateContextHolder.java | 549 ++++ .../help/context/LicenseContextHolder.java | 223 ++ .../help/context/PluginsContextHolder.java | 347 +++ .../help/context/ProductsContextHolder.java | 179 ++ .../qiumo/help/controller/DataController.java | 222 ++ .../help/controller/JRebelController.java | 54 + .../controller/LicenseCodeController.java | 228 ++ .../controller/LicenseServerController.java | 233 ++ .../qiumo/help/controller/ZipController.java | 97 + .../help/model/ObtainTicketResponse.java | 88 + .../com/qiumo/help/model/PingResponse.java | 61 + .../help/model/ProlongTicketResponse.java | 61 + .../help/model/ReleaseTicketResponse.java | 61 + .../com/qiumo/help/service/JrebelService.java | 103 + .../java/com/qiumo/help/util/FileTools.java | 144 + .../qiumo/help/util/LicenseServerUtils.java | 250 ++ .../com/qiumo/help/util/jrebel/ByteUtil.java | 34 + .../qiumo/help/util/jrebel/JrebelSign.java | 30 + .../LicenseServer2ToJRebelPrivateKey.java | 44 + src/main/resources/application.yml | 9 + src/main/resources/banner.txt | 8 + .../resources/external/agent/ja-netfilter.zip | Bin 0 -> 85423 bytes .../external/certificate/code-root.key | 30 + .../external/certificate/server-root.key | 31 + src/main/resources/external/data/plugin.json | 2597 +++++++++++++++++ src/main/resources/external/data/product.json | 77 + src/main/resources/logback-spring.xml | 121 + src/main/resources/static/css/main.css | 328 +++ src/main/resources/static/images/aqua.svg | 1 + src/main/resources/static/images/clion.svg | 1 + src/main/resources/static/images/datagrip.svg | 1 + .../resources/static/images/dataspell.svg | 1 + src/main/resources/static/images/dotcover.svg | 1 + .../resources/static/images/dotmemory.svg | 1 + src/main/resources/static/images/dottrace.svg | 1 + src/main/resources/static/images/goland.svg | 1 + src/main/resources/static/images/icons.svg | 713 +++++ .../resources/static/images/intellij-idea.svg | 1 + src/main/resources/static/images/phpstorm.svg | 1 + src/main/resources/static/images/plugin.svg | 1 + src/main/resources/static/images/pycharm.svg | 1 + src/main/resources/static/images/rider.svg | 1 + src/main/resources/static/images/rubymine.svg | 1 + .../resources/static/images/rustrover.svg | 1 + src/main/resources/static/images/webstorm.svg | 1 + src/main/resources/static/index.html | 515 ++++ src/main/resources/static/js/app.js | 247 ++ src/main/resources/static/js/utils.js | 209 ++ .../QiuMoJetbrainsHelpApplicationTests.java | 13 + 56 files changed, 8593 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 pom.xml create mode 100644 src/main/java/com/qiumo/help/QiuMoJetbrainsHelpApplication.java create mode 100644 src/main/java/com/qiumo/help/context/AgentContextHolder.java create mode 100644 src/main/java/com/qiumo/help/context/CertificateContextHolder.java create mode 100644 src/main/java/com/qiumo/help/context/LicenseContextHolder.java create mode 100644 src/main/java/com/qiumo/help/context/PluginsContextHolder.java create mode 100644 src/main/java/com/qiumo/help/context/ProductsContextHolder.java create mode 100644 src/main/java/com/qiumo/help/controller/DataController.java create mode 100644 src/main/java/com/qiumo/help/controller/JRebelController.java create mode 100644 src/main/java/com/qiumo/help/controller/LicenseCodeController.java create mode 100644 src/main/java/com/qiumo/help/controller/LicenseServerController.java create mode 100644 src/main/java/com/qiumo/help/controller/ZipController.java create mode 100644 src/main/java/com/qiumo/help/model/ObtainTicketResponse.java create mode 100644 src/main/java/com/qiumo/help/model/PingResponse.java create mode 100644 src/main/java/com/qiumo/help/model/ProlongTicketResponse.java create mode 100644 src/main/java/com/qiumo/help/model/ReleaseTicketResponse.java create mode 100644 src/main/java/com/qiumo/help/service/JrebelService.java create mode 100644 src/main/java/com/qiumo/help/util/FileTools.java create mode 100644 src/main/java/com/qiumo/help/util/LicenseServerUtils.java create mode 100644 src/main/java/com/qiumo/help/util/jrebel/ByteUtil.java create mode 100644 src/main/java/com/qiumo/help/util/jrebel/JrebelSign.java create mode 100644 src/main/java/com/qiumo/help/util/jrebel/LicenseServer2ToJRebelPrivateKey.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/external/agent/ja-netfilter.zip create mode 100644 src/main/resources/external/certificate/code-root.key create mode 100644 src/main/resources/external/certificate/server-root.key create mode 100644 src/main/resources/external/data/plugin.json create mode 100644 src/main/resources/external/data/product.json create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/static/css/main.css create mode 100644 src/main/resources/static/images/aqua.svg create mode 100644 src/main/resources/static/images/clion.svg create mode 100644 src/main/resources/static/images/datagrip.svg create mode 100644 src/main/resources/static/images/dataspell.svg create mode 100644 src/main/resources/static/images/dotcover.svg create mode 100644 src/main/resources/static/images/dotmemory.svg create mode 100644 src/main/resources/static/images/dottrace.svg create mode 100644 src/main/resources/static/images/goland.svg create mode 100644 src/main/resources/static/images/icons.svg create mode 100644 src/main/resources/static/images/intellij-idea.svg create mode 100644 src/main/resources/static/images/phpstorm.svg create mode 100644 src/main/resources/static/images/plugin.svg create mode 100644 src/main/resources/static/images/pycharm.svg create mode 100644 src/main/resources/static/images/rider.svg create mode 100644 src/main/resources/static/images/rubymine.svg create mode 100644 src/main/resources/static/images/rustrover.svg create mode 100644 src/main/resources/static/images/webstorm.svg create mode 100644 src/main/resources/static/index.html create mode 100644 src/main/resources/static/js/app.js create mode 100644 src/main/resources/static/js/utils.js create mode 100644 src/test/java/com/qiumo/help/QiuMoJetbrainsHelpApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c341a2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +target/ +logs/ +.claude/ +CLAUDE.md +ui.md +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6151509 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM maven:3-openjdk-8 as build +WORKDIR /app +COPY . . +RUN mvn clean package + +FROM openjdk:8-jre-alpine +WORKDIR /app +COPY --from=build /app/target/QiuMo-Jetbrains-Help.jar QiuMo-Jetbrains-Help.jar +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +EXPOSE 10768 +ENTRYPOINT ["java", "-jar", "QiuMo-Jetbrains-Help.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8910542 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# QiuMo JetBrains Help + +一个基于Spring Boot的JetBrains许可证激活服务项目。 + +## 功能特性 + +- 模拟JetBrains官方许可证服务器 +- 生成产品和插件激活码 +- 提供JRebel许可证服务 +- 集成ja-netfilter代理工具下载 +- 现代化Web界面 + +## 快速开始 + +### 环境要求 + +- Java 1.8+ +- Maven 3.x + +### 运行应用 + +```bash +# 克隆项目 +git clone +cd QiuMo-Jetbrains-Help + +# 编译项目 +mvn clean compile + +# 启动应用 +mvn spring-boot:run +``` + +应用启动后访问:http://localhost:10768 + +## 使用说明 + +1. **下载代理工具**:访问 `/ja-netfilter` 下载并配置ja-netfilter +2. **激活码方式**:在Web界面生成产品或插件激活码 +3. **服务器方式**:配置许可证服务器地址为应用根地址 +4. **JRebel激活**:使用 `{服务器地址}/{uuid}` 格式 + +## 技术栈 + +- **后端**:Spring Boot 2.6.13, Java 1.8 +- **前端**:Vue.js 3, TailwindCSS +- **构建**:Maven + +## 许可证 + +本项目仅供学习交流使用。 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..620b51a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + qiumo-jetbrains-help: + build: . + image: qiumo-jetbrains-help:latest + container_name: QiuMo-Jetbrains-Help + ports: + - 10768:10768 + volumes: + - ./external:/app/external + - ./logs:/app/logs diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..10bd13a --- /dev/null +++ b/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + + com.qiumo + QiuMo-Jetbrains-Help + 0.0.1-SNAPSHOT + jar + QiuMo-Jetbrains-Help + QiuMo Jetbrains Help Application + + + + 1.8 + 1.8 + 1.8 + UTF-8 + UTF-8 + + + 2.6.13 + + + 1.18.24 + 5.8.11 + 1.78 + 1.2.12 + + + 3.8.1 + 2.22.2 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + + + org.fusesource.jansi + jansi + 2.4.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + ${artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + com.qiumo.help.QiuMoJetbrainsHelpApplication + + + org.projectlombok + lombok + + + + + + + repackage + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + diff --git a/src/main/java/com/qiumo/help/QiuMoJetbrainsHelpApplication.java b/src/main/java/com/qiumo/help/QiuMoJetbrainsHelpApplication.java new file mode 100644 index 0000000..1de7f47 --- /dev/null +++ b/src/main/java/com/qiumo/help/QiuMoJetbrainsHelpApplication.java @@ -0,0 +1,166 @@ +package com.qiumo.help; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.qiumo.help.context.AgentContextHolder; +import com.qiumo.help.context.CertificateContextHolder; +import com.qiumo.help.context.PluginsContextHolder; +import com.qiumo.help.context.ProductsContextHolder; +import java.net.InetAddress; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * QiuMo JetBrains Help 应用程序主入口类 + * + *

这是一个Spring Boot应用程序,用于提供JetBrains产品的许可证激活服务。 + * 该应用程序主要功能包括: + *

+ * + *

应用启动后将在指定端口(默认10768)提供HTTP服务,用于处理许可证相关请求。 + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "项目入口") +@EnableScheduling // 启用Spring任务调度功能 +@Import(SpringUtil.class) // 导入Hutool的Spring工具类 +@SpringBootApplication +public class QiuMoJetbrainsHelpApplication { + + /** + * 应用程序主入口方法 + * + * @param args 命令行参数 + */ + public static void main(String[] args) { + // 启动Spring Boot应用程序 + SpringApplication.run(QiuMoJetbrainsHelpApplication.class, args); + } + + /** + * 应用程序准备就绪事件监听器 + * + *

当Spring Boot应用程序完全启动并准备好接收请求时触发此方法。 + * 该方法负责初始化所有必要的上下文组件和输出启动成功信息。 + * + *

初始化顺序说明: + *

    + *
  1. 产品上下文 - 初始化JetBrains产品信息
  2. + *
  3. 插件上下文 - 初始化JetBrains插件信息
  4. + *
  5. 证书上下文 - 初始化RSA密钥对和X.509证书
  6. + *
  7. 代理上下文 - 初始化ja-netfilter代理配置
  8. + *
+ * + * @throws Exception 当初始化过程中发生错误时抛出异常 + */ + @SneakyThrows + @EventListener(ApplicationReadyEvent.class) + public void ready() { + log.info("开始初始化应用程序组件..."); + + // 按顺序初始化各个上下文组件 + ProductsContextHolder.init(); // 初始化产品信息上下文 + PluginsContextHolder.init(); // 初始化插件信息上下文 + CertificateContextHolder.init(); // 初始化证书和密钥上下文 + AgentContextHolder.init(); // 初始化ja-netfilter代理上下文 + + // 获取本地IP地址和端口号,构建访问URL + InetAddress localHost = InetAddress.getLocalHost(); + String serverPort = SpringUtil.getProperty("server.port"); + String accessUrl = CharSequenceUtil.format("http://{}:{}", localHost.getHostAddress(), serverPort); + + // 输出应用启动成功信息 + String startupSuccessMessage = buildStartupSuccessMessage(accessUrl); + log.info(startupSuccessMessage); + } + + /** + * 定时刷新插件信息任务 + * + *

每天中午12点执行一次,从JetBrains官网获取最新的插件信息。 + * 此任务采用异步执行方式,不会阻塞应用程序的正常运行。 + * + *

Cron表达式说明:"0 0 12 * * ?" + *

+ */ + @Scheduled(cron = "0 0 12 * * ?") + public void refresh() { + log.info("开始执行定时刷新插件信息任务..."); + // 异步执行插件信息刷新任务,避免阻塞主线程 + ThreadUtil.execute(PluginsContextHolder::refreshJsonFile); + } + + /** + * 构建应用启动成功消息 + * + * @param accessUrl 应用访问地址 + * @return 格式化的启动成功消息 + */ + private String buildStartupSuccessMessage(String accessUrl) { + return "\n" + + "============================================================================================\n" + + "= QiuMo-Jetbrains-Help 启动成功! 🎉 =\n" + + "= =\n" + + String.format("= 访问地址: %-70s =\n", accessUrl) + + "= =\n" + + "= 功能说明: =\n" + + "= • 生成JetBrains产品许可证 =\n" + + "= • 模拟许可证服务器 =\n" + + "= • 下载ja-netfilter代理工具 =\n" + + "= =\n" + + "============================================================================================\n"; + } + + /** + * 跨域资源共享(CORS)配置 + * + *

为了支持前端页面调用后端 API,需要配置 CORS 策略。 + * 这个配置允许来自任何域名的请求访问 API。 + * + * @return WebMvc配置器,用于设置跨域访问规则 + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") // 允许所有来源 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的HTTP方法 + .allowedHeaders("*") // 允许所有请求头 + .maxAge(3600); // 预检请求的有效期(1小时) + } + }; + } + +} diff --git a/src/main/java/com/qiumo/help/context/AgentContextHolder.java b/src/main/java/com/qiumo/help/context/AgentContextHolder.java new file mode 100644 index 0000000..f2f0030 --- /dev/null +++ b/src/main/java/com/qiumo/help/context/AgentContextHolder.java @@ -0,0 +1,241 @@ +package com.qiumo.help.context; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ZipUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.PemUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.qiumo.help.util.FileTools; +import java.io.File; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.concurrent.CompletableFuture; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.util.encoders.Hex; + +/** + * ja-netfilter代理上下文管理器 + * + *

此类负责管理ja-netfilter代理工具的初始化、配置和打包。 + * ja-netfilter是一个基于Java Instrumentation API实现的动态字节码修改工具, + * 主要用于在应用程序运行时动态修改字节码,实现特定的功能。 + * + *

主要功能: + *

+ * + *

配置规则生成: + *

+ * + *

文件结构: + *

+ * external/agent/
+ * ├── ja-netfilter.zip     // 原始工具包
+ * └── ja-netfilter/        // 解压后的目录
+ *     ├── ja-netfilter.jar // 主程序
+ *     ├── plugins/         // 插件目录
+ *     └── config/          // 配置目录
+ *         └── power.conf   // 主配置文件
+ * 
+ * + *

使用方法: + *

    + *
  1. 下载并解压ja-netfilter工具包
  2. + *
  3. 在JVM启动参数中添加:{@code -javaagent:path/to/ja-netfilter.jar}
  4. + *
  5. 重新启动目标应用程序
  6. + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "代理上下文") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AgentContextHolder { + + // ==================== 常量定义 ==================== + + /** ja-netfilter工具文件夹路径 */ + private static final String JA_NETFILTER_FILE_PATH = "external/agent/ja-netfilter"; + + /** ja-netfilter主配置文件路径 */ + private static final String POWER_CONF_FILE_NAME = JA_NETFILTER_FILE_PATH + "/config/power.conf"; + + // ==================== 静态字段 ==================== + + /** ja-netfilter工具文件夹对象 */ + private static File jaNetfilterFile; + + /** ja-netfilter ZIP包文件对象 */ + private static File jaNetfilterZipFile; + + // ==================== 核心方法 ==================== + + public static void init() { + log.info("初始化中..."); + jaNetfilterZipFile = FileTools.getFileOrCreat(JA_NETFILTER_FILE_PATH + ".zip"); + if (!FileTools.fileExists(JA_NETFILTER_FILE_PATH)) { + unzipJaNetfilter(); + if (!powerConfHasInit()) { + log.info("配置初始化中..."); + loadPowerConf(); + zipJaNetfilter(); + log.info("配置初始化成功!"); + } + } + log.info("初始化成功!"); + } + + public static File jaNetfilterZipFile() { + return AgentContextHolder.jaNetfilterZipFile; + } + + private static void unzipJaNetfilter() { + jaNetfilterFile = ZipUtil.unzip(jaNetfilterZipFile); + } + + private static void zipJaNetfilter() { + jaNetfilterZipFile = ZipUtil.zip(jaNetfilterFile); + } + + private static boolean powerConfHasInit() { + File powerConfFile = FileTools.getFileOrCreat(POWER_CONF_FILE_NAME); + String powerConfStr; + try { + powerConfStr = IoUtil.readUtf8(FileUtil.getInputStream(powerConfFile)); + } catch (IORuntimeException e) { + throw new IllegalArgumentException(CharSequenceUtil.format("{} 文件读取失败!", POWER_CONF_FILE_NAME), e); + } + return CharSequenceUtil.containsAll(powerConfStr, "[Result]", "EQUAL,"); + } + + private static void loadPowerConf() { + CompletableFuture + .supplyAsync(AgentContextHolder::generateCodePowerConfigRule) + .thenApply(AgentContextHolder::generateServerPowerConfigRule) + .thenApply(AgentContextHolder::generatePowerConfigStr) + .thenAccept(AgentContextHolder::overridePowerConfFileContent) + .exceptionally(throwable -> { + log.error("配置初始化失败!", throwable); + return null; + }).join(); + } + + @SneakyThrows + private static String generateCodePowerConfigRule() { + X509Certificate crt = (X509Certificate) KeyUtil.readX509Certificate( + IoUtil.toStream(CertificateContextHolder.codeCrtFile())); + RSAPublicKey publicKey = (RSAPublicKey) PemUtil.readPemPublicKey( + IoUtil.toStream(CertificateContextHolder.publicKeyFile())); + RSAPublicKey rootPublicKey = (RSAPublicKey) PemUtil.readPemPublicKey( + IoUtil.toStream(CertificateContextHolder.codeRootKeyFile())); + BigInteger x = new BigInteger(1, crt.getSignature()); + BigInteger y = BigInteger.valueOf(65537L); + BigInteger z = rootPublicKey.getModulus(); + BigInteger r = x.modPow(publicKey.getPublicExponent(), publicKey.getModulus()); + return CharSequenceUtil.format("; QiuMo Activation Code \nEQUAL,{},{},{}->{}", x, y, z, r); + } + + @SneakyThrows + private static String generateServerPowerConfigRule(String ruleValue) { + X509Certificate crt = (X509Certificate) KeyUtil.readX509Certificate( + IoUtil.toStream(CertificateContextHolder.serverChildCrtFile())); + RSAPublicKey rootPublicKey = (RSAPublicKey) PemUtil.readPemPublicKey( + IoUtil.toStream(CertificateContextHolder.serverRootKeyFile())); + BigInteger x = new BigInteger(1, crt.getSignature()); + BigInteger y = BigInteger.valueOf(65537L); + BigInteger z = rootPublicKey.getModulus(); + byte[] tbsCertificate = crt.getTBSCertificate(); + // 1、证书sha256摘要结果 + byte[] bytes = DigestUtil.sha256(tbsCertificate); + String sha256Str = HexUtil.encodeHexStr(bytes); + // 2、计算的结果转换为ASN1格式数据 + String transit = convertDataToASN1Format(sha256Str); + // 3、ASN1格式数据再进行填充 + String fillingStr = filling512(transit); + // 4、填充后的数据转换为BigInteger数据,BigInteger输出的结果就是规则中替换的结果。 + BigInteger r = new BigInteger(HexUtil.decodeHex(fillingStr)); + return CharSequenceUtil.format("{}\n; QiuMo License Server \nEQUAL,{},{},{}->{}", ruleValue,x, y, z, r); + } + + + private static String generatePowerConfigStr(String ruleValue) { + return CharSequenceUtil.builder("[Result]", "\n", ruleValue).toString(); + } + + private static void overridePowerConfFileContent(String configStr) { + File powerConfFile = FileTools.getFileOrCreat(POWER_CONF_FILE_NAME); + try { + FileUtil.writeString(configStr, powerConfFile, StandardCharsets.UTF_8); + } catch (IORuntimeException e) { + throw new IllegalArgumentException(CharSequenceUtil.format("{} 文件写入失败!", POWER_CONF_FILE_NAME), e); + } + } + + /** + * 证书sha256摘要结果转换为ASN1格式数据 + * + * @param sha256Data 证书sha256摘要结果 + * @return + */ + public static String convertDataToASN1Format(String sha256Data) throws Exception { + // 构建内层 SEQUENCE + ASN1ObjectIdentifier algorithmOid = new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1"); // sha-256 + ASN1Encodable[] innerSequenceElements = {algorithmOid, DERNull.INSTANCE}; + DERSequence innerSequence = new DERSequence(innerSequenceElements); + + // 构建外层 SEQUENCE + byte[] octetStringBytes = Hex.decode(sha256Data); + ASN1Encodable octetString = new DEROctetString(octetStringBytes); + ASN1Encodable[] outerSequenceElements = {innerSequence, octetString}; + DERSequence outerSequence = new DERSequence(outerSequenceElements); + + // 将ASN.1结构编码为DER格式 + byte[] encodedData = outerSequence.getEncoded(); + + // 将字节数组转换为十六进制字符串 + String hexString = Hex.toHexString(encodedData); + return hexString; + } + + private static String filling512(String target) { + return filling(target, 512); + } + + private static String filling256(String target) { + return filling(target, 256); + } + + private static String filling(String target, int length) { + int count = length - target.length() / 2 - 3; + StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < count; i++) { + strBuilder.append("ff"); + } + return ("01" + strBuilder + "00" + target).toUpperCase(); + } + +} diff --git a/src/main/java/com/qiumo/help/context/CertificateContextHolder.java b/src/main/java/com/qiumo/help/context/CertificateContextHolder.java new file mode 100644 index 0000000..83d9e0a --- /dev/null +++ b/src/main/java/com/qiumo/help/context/CertificateContextHolder.java @@ -0,0 +1,549 @@ +package com.qiumo.help.context; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.PemUtil; +import cn.hutool.crypto.SecureUtil; +import com.qiumo.help.util.FileTools; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.GeneralNamesBuilder; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * 证书上下文持有者 - 负责管理RSA密钥对和X.509证书的生成、存储和访问 + * + *

此类作为证书管理的核心工具,提供以下主要功能: + *

+ * + *

使用场景: + * 主要用于JetBrains产品的许可证服务器搭建,生成必要的证书文件用于许可证验证。 + * + *

证书体系结构: + *

+ * + * @author QiuMo + * @version 1.0 + * @since 1.0 + */ +@Slf4j(topic = "证书上下文") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CertificateContextHolder { + + // ==================== 常量定义 ==================== + + /** 授权码根密钥文件路径 */ + private static final String CODE_ROOT_KEY_FILE_NAME = "external/certificate/code-root.key"; + + /** 服务器根密钥文件路径 */ + private static final String SERVER_ROOT_KEY_FILE_NAME = "external/certificate/server-root.key"; + + /** 4096位私钥文件路径 */ + private static final String PRIVATE_KEY_FILE_NAME = "external/certificate/private.key"; + + /** 4096位公钥文件路径 */ + private static final String PUBLIC_KEY_FILE_NAME = "external/certificate/public.key"; + + /** 授权码证书文件路径 */ + private static final String CODE_CET_FILE_NAME = "external/certificate/code-ca.crt"; + + /** 服务器证书文件路径 */ + private static final String SERVER_CET_FILE_NAME = "external/certificate/server-ca.crt"; + + /** 服务器子证书文件路径 */ + private static final String SERVER_CHILD_CET_FILE_NAME = "external/certificate/server-child-ca.crt"; + + /** 证书有效期(年) */ + private static final int CERTIFICATE_VALIDITY_YEARS = 100; + + /** RSA签名算法 */ + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; + + /** BouncyCastle提供者标识 */ + private static final String BC_PROVIDER = "BC"; + + /** RSA加密算法标识 */ + private static final String RSA_ALGORITHM = "RSA"; + + /** 授权码签名CA的DN */ + private static final String CODE_CA_DN = "CN=JetProfile CA"; + + /** 许可证服务器CA的DN */ + private static final String LICENSE_SERVER_CA_DN = "CN=License Servers CA"; + + /** 应用程序主体DN */ + private static final String APP_SUBJECT_DN = "CN=QiuMo-Jetbrains-Help"; + + /** 许可证服务器域名 */ + private static final String LICENSE_SERVER_DOMAIN = "qiumo.lsrv.jetbrains.com"; + + /** 许可证服务器主体DN */ + private static final String LICENSE_SERVER_SUBJECT_DN = "CN=" + LICENSE_SERVER_DOMAIN; + + // ==================== 静态文件对象 ==================== + + /** 授权码根密钥文件对象 */ + private static File codeRootKeyFile; + + /** 服务器根密钥文件对象 */ + private static File serverRootKeyFile; + + /** 4096位私钥文件对象 */ + private static File privateKeyFile; + + /** 4096位公钥文件对象 */ + private static File publicKeyFile; + + /** 授权码证书文件对象 */ + private static File codeCrtFile; + + /** 服务器证书文件对象 */ + private static File serverCrtFile; + + /** 服务器子证书文件对象 */ + private static File serverChildCrtFile; + + // ==================== 初始化方法 ==================== + + /** + * 初始化证书上下文 + * + *

此方法执行以下操作: + *

    + *
  1. 创建根密钥文件(如果不存在)
  2. + *
  3. 检查所需的密钥和证书文件是否存在
  4. + *
  5. 如果文件缺失,自动生成完整的证书体系
  6. + *
  7. 初始化所有文件对象引用
  8. + *
+ * + * @throws RuntimeException 当文件操作或证书生成失败时抛出 + */ + public static void init() { + log.info("证书上下文初始化开始..."); + + try { + // 初始化根密钥文件 + initializeRootKeyFiles(); + + // 检查并生成必要的证书文件 + if (isCertificateGenerationRequired()) { + log.info("检测到证书文件缺失,开始生成证书..."); + generateCertificate(); + log.info("证书生成完成!"); + } else { + log.info("证书文件已存在,直接加载..."); + initializeExistingFiles(); + } + + log.info("证书上下文初始化成功!"); + + } catch (Exception e) { + log.error("证书上下文初始化失败", e); + throw new RuntimeException("证书初始化异常", e); + } + } + + /** + * 初始化根密钥文件 + * 创建授权码根密钥和服务器根密钥文件(如果不存在) + */ + private static void initializeRootKeyFiles() { + codeRootKeyFile = FileTools.getFileOrCreat(CODE_ROOT_KEY_FILE_NAME); + serverRootKeyFile = FileTools.getFileOrCreat(SERVER_ROOT_KEY_FILE_NAME); + log.debug("根密钥文件初始化完成"); + } + + /** + * 检查是否需要生成证书 + * 如果任意一个必需文件不存在,则需要重新生成所有证书 + * + * @return 如果任意一个必需文件不存在则返回true + */ + private static boolean isCertificateGenerationRequired() { + return !FileTools.fileExists(PRIVATE_KEY_FILE_NAME) + || !FileTools.fileExists(PUBLIC_KEY_FILE_NAME) + || !FileTools.fileExists(CODE_CET_FILE_NAME) + || !FileTools.fileExists(SERVER_CET_FILE_NAME) + || !FileTools.fileExists(SERVER_CHILD_CET_FILE_NAME); + } + + /** + * 初始化已存在的文件对象 + * 当所有证书文件都存在时,直接创建文件对象引用 + */ + private static void initializeExistingFiles() { + privateKeyFile = FileTools.getFileOrCreat(PRIVATE_KEY_FILE_NAME); + publicKeyFile = FileTools.getFileOrCreat(PUBLIC_KEY_FILE_NAME); + codeCrtFile = FileTools.getFileOrCreat(CODE_CET_FILE_NAME); + serverCrtFile = FileTools.getFileOrCreat(SERVER_CET_FILE_NAME); + serverChildCrtFile = FileTools.getFileOrCreat(SERVER_CHILD_CET_FILE_NAME); + log.debug("已存在文件对象初始化完成"); + } + + // ==================== 文件访问方法 ==================== + + /** + * 获取授权码根密钥文件对象 + * + * @return 授权码根密钥文件 + */ + public static File codeRootKeyFile() { + return codeRootKeyFile; + } + + /** + * 获取服务器根密钥文件对象 + * + * @return 服务器根密钥文件 + */ + public static File serverRootKeyFile() { + return serverRootKeyFile; + } + + /** + * 获取4096位私钥文件对象 + * + * @return 4096位私钥文件 + */ + public static File privateKeyFile() { + return privateKeyFile; + } + + /** + * 获取4096位公钥文件对象 + * + * @return 4096位公钥文件 + */ + public static File publicKeyFile() { + return publicKeyFile; + } + + /** + * 获取授权码证书文件对象 + * + * @return 授权码证书文件 + */ + public static File codeCrtFile() { + return codeCrtFile; + } + + /** + * 获取服务器证书文件对象 + * + * @return 服务器证书文件 + */ + public static File serverCrtFile() { + return serverCrtFile; + } + + /** + * 获取服务器子证书文件对象 + * + * @return 服务器子证书文件 + */ + public static File serverChildCrtFile() { + return serverChildCrtFile; + } + + // ==================== 证书生成方法 ==================== + + /** + * 生成完整的证书体系 + * + *

此方法将生成以下内容: + *

+ * + * @throws RuntimeException 当密钥生成或证书创建失败时抛出 + */ + public static void generateCertificate() { + log.info("开始生成证书体系..."); + + try { + // 生成密钥对 + log.debug("正在生成RSA密钥对..."); + KeyPair keyPair4096 = generateRSAKeyPair(4096); + + // 保存密钥对到文件 + saveKeyPairToFiles(keyPair4096, PRIVATE_KEY_FILE_NAME, PUBLIC_KEY_FILE_NAME); + + // 生成证书 + log.debug("正在生成X.509证书..."); + generateCertificates(keyPair4096); + + log.info("证书体系生成完成"); + + } catch (Exception e) { + log.error("证书生成过程中发生错误", e); + throw new RuntimeException("证书生成失败", e); + } + } + + /** + * 生成指定长度的RSA密钥对 + * + * @param keySize RSA密钥长度(位) + * @return 生成的RSA密钥对 + * @throws RuntimeException 当密钥生成失败时抛出 + */ + private static KeyPair generateRSAKeyPair(int keySize) { + try { + log.debug("生成{}位RSA密钥对", keySize); + return SecureUtil.generateKeyPair(RSA_ALGORITHM, keySize); + } catch (Exception e) { + throw new RuntimeException("RSA密钥对生成失败: " + keySize + "位", e); + } + } + + /** + * 将密钥对保存到指定的文件中 + * + * @param keyPair 要保存的密钥对 + * @param privateKeyPath 私钥文件路径 + * @param publicKeyPath 公钥文件路径 + */ + private static void saveKeyPairToFiles(KeyPair keyPair, String privateKeyPath, String publicKeyPath) { + try { + // 保存私钥 + File privateKeyFileObj = FileTools.getFileOrCreat(privateKeyPath); + PemUtil.writePemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded(), + FileUtil.getWriter(privateKeyFileObj, StandardCharsets.UTF_8, false)); + + // 保存公钥 + File publicKeyFileObj = FileTools.getFileOrCreat(publicKeyPath); + PemUtil.writePemObject("PUBLIC KEY", keyPair.getPublic().getEncoded(), + FileUtil.getWriter(publicKeyFileObj, StandardCharsets.UTF_8, false)); + + publicKeyFile = publicKeyFileObj; + privateKeyFile = privateKeyFileObj; + + log.debug("密钥对已保存: {} 和 {}", privateKeyPath, publicKeyPath); + + } catch (Exception e) { + throw new RuntimeException("密钥对文件保存失败", e); + } + } + + /** + * 生成所有类型的X.509证书 + * + * @param keyPair 4096位密钥对 + */ + private static void generateCertificates(KeyPair keyPair) { + try { + PrivateKey privateKey4096 = keyPair.getPrivate(); + PublicKey publicKey4096 = keyPair.getPublic(); + + // 创建内容签名器 + ContentSigner signer = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).build(privateKey4096); + JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER); + + // 生成授权码签名CA证书 + Certificate codeCertificate = generateCodeCACertificate(publicKey4096, signer, certConverter); + + // 生成许可证服务器CA证书 + Certificate serverCertificate = generateServerCACertificate(publicKey4096, signer, certConverter); + + // 生成服务器子证书(具有特殊扩展) + Certificate childCertificate = generateServerChildCertificate(publicKey4096, signer, certConverter); + + // 保存所有证书到文件 + saveCertificatesToFiles(codeCertificate, serverCertificate, childCertificate); + + } catch (Exception e) { + throw new RuntimeException("X.509证书生成失败", e); + } + } + + /** + * 生成授权码签名CA证书 + * + * @param publicKey 公钥 + * @param signer 内容签名器 + * @param certConverter 证书转换器 + * @return 生成的授权码签名CA证书 + */ + private static Certificate generateCodeCACertificate(PublicKey publicKey, ContentSigner signer, + JcaX509CertificateConverter certConverter) throws Exception { + JcaX509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder( + new X500Name(CODE_CA_DN), // 颁发者DN + BigInteger.valueOf(System.currentTimeMillis()), // 序列号 + DateUtil.yesterday(), // 生效日期(昨天,避免时钟偏差) + DateUtil.date().offset(DateField.YEAR, CERTIFICATE_VALIDITY_YEARS), // 过期日期 + new X500Name(APP_SUBJECT_DN), // 主体DN + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded())); // 公钥信息 + + return certConverter.getCertificate(certificateBuilder.build(signer)); + } + + /** + * 生成许可证服务器CA证书 + * + * @param publicKey 公钥 + * @param signer 内容签名器 + * @param certConverter 证书转换器 + * @return 生成的许可证服务器CA证书 + */ + private static Certificate generateServerCACertificate(PublicKey publicKey, ContentSigner signer, + JcaX509CertificateConverter certConverter) throws Exception { + JcaX509v3CertificateBuilder serversCertBuilder = new JcaX509v3CertificateBuilder( + new X500Name(LICENSE_SERVER_CA_DN), // 颁发者DN + BigInteger.valueOf(System.currentTimeMillis()), // 序列号 + DateUtil.yesterday(), // 生效日期 + DateUtil.date().offset(DateField.YEAR, CERTIFICATE_VALIDITY_YEARS), // 过期日期 + new X500Name(APP_SUBJECT_DN), // 主体DN + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded())); // 公钥信息 + + return certConverter.getCertificate(serversCertBuilder.build(signer)); + } + + /** + * 生成服务器子证书(包含扩展属性) + * + *

此证书包含以下扩展属性: + *

+ * + * @param publicKey4096 4096位公钥(用于权威密钥标识符) + * @param signer 内容签名器 + * @param certConverter 证书转换器 + * @return 生成的服务器子证书 + */ + private static Certificate generateServerChildCertificate(PublicKey publicKey4096, + ContentSigner signer, JcaX509CertificateConverter certConverter) throws Exception { + JcaX509v3CertificateBuilder childCertBuilder = new JcaX509v3CertificateBuilder( + new X500Name(LICENSE_SERVER_CA_DN), // 颁发者DN + BigInteger.valueOf(System.currentTimeMillis()), // 序列号 + DateUtil.yesterday(), // 生效日期 + DateUtil.date().offset(DateField.YEAR, CERTIFICATE_VALIDITY_YEARS), // 过期日期 + new X500Name(LICENSE_SERVER_SUBJECT_DN), // 主体DN(具体域名) + SubjectPublicKeyInfo.getInstance(publicKey4096.getEncoded())); // 公钥信息 + + // 添加证书扩展 + addCertificateExtensions(childCertBuilder, publicKey4096); + + return certConverter.getCertificate(childCertBuilder.build(signer)); + } + + /** + * 为服务器子证书添加扩展属性 + * + * @param certBuilder 证书构建器 + * @param publicKey4096 4096位公钥 + */ + private static void addCertificateExtensions(JcaX509v3CertificateBuilder certBuilder,PublicKey publicKey4096) throws Exception { + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + + // 1. 扩展密钥用途:服务器认证和客户端认证 + certBuilder.addExtension(Extension.extendedKeyUsage, false, + new ExtendedKeyUsage(new KeyPurposeId[]{ + KeyPurposeId.id_kp_serverAuth, // 服务器认证 + KeyPurposeId.id_kp_clientAuth // 客户端认证 + })); + + // 2. 主体备用名称:DNS域名 + GeneralNamesBuilder generalNamesBuilder = new GeneralNamesBuilder(); + GeneralName dnsName = new GeneralName(GeneralName.dNSName, LICENSE_SERVER_DOMAIN); + generalNamesBuilder.addName(dnsName); + GeneralNames generalNames = generalNamesBuilder.build(); + certBuilder.addExtension(Extension.subjectAlternativeName, false, generalNames); + + // 3. 密钥用途:数字签名、密钥加密、密钥协商 + certBuilder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.keyAgreement)); + + // 4. 权威密钥标识符(基于服务器根证书) + X509Certificate templateCert = (X509Certificate) KeyUtil.readX509Certificate( + IoUtil.toStream(serverRootKeyFile)); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, + extUtils.createAuthorityKeyIdentifier( + SubjectPublicKeyInfo.getInstance(templateCert.getPublicKey().getEncoded()))); + + // 5. 主体密钥标识符(基于2048位公钥) + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, + extUtils.createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(publicKey4096.getEncoded()))); + + log.debug("服务器子证书扩展属性添加完成"); + } + + /** + * 将生成的证书保存到对应的文件中 + * + * @param codeCertificate 授权码签名CA证书 + * @param serverCertificate 许可证服务器CA证书 + * @param childCertificate 服务器子证书 + */ + private static void saveCertificatesToFiles(Certificate codeCertificate, Certificate serverCertificate, + Certificate childCertificate) throws Exception { + // 创建证书文件对象 + codeCrtFile = FileTools.getFileOrCreat(CODE_CET_FILE_NAME); + serverCrtFile = FileTools.getFileOrCreat(SERVER_CET_FILE_NAME); + serverChildCrtFile = FileTools.getFileOrCreat(SERVER_CHILD_CET_FILE_NAME); + + // 保存授权码签名CA证书 + PemUtil.writePemObject("CERTIFICATE", codeCertificate.getEncoded(), + FileUtil.getWriter(codeCrtFile, StandardCharsets.UTF_8, false)); + + // 保存许可证服务器CA证书 + PemUtil.writePemObject("CERTIFICATE", serverCertificate.getEncoded(), + FileUtil.getWriter(serverCrtFile, StandardCharsets.UTF_8, false)); + + // 保存服务器子证书 + PemUtil.writePemObject("CERTIFICATE", childCertificate.getEncoded(), + FileUtil.getWriter(serverChildCrtFile, StandardCharsets.UTF_8, false)); + + log.debug("所有证书文件保存完成"); + } + +} diff --git a/src/main/java/com/qiumo/help/context/LicenseContextHolder.java b/src/main/java/com/qiumo/help/context/LicenseContextHolder.java new file mode 100644 index 0000000..11eb6b6 --- /dev/null +++ b/src/main/java/com/qiumo/help/context/LicenseContextHolder.java @@ -0,0 +1,223 @@ +package com.qiumo.help.context; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.crypto.PemUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.SignUtil; +import cn.hutool.crypto.asymmetric.Sign; +import cn.hutool.json.JSONUtil; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.List; +import java.util.Set; + +import static cn.hutool.crypto.asymmetric.SignAlgorithm.SHA1withRSA; + +/** + * 许可证上下文管理器 + * + *

此类为整个应用程序的核心组件之一,专门负责JetBrains产品激活码的生成。 + * 它封装了复杂的密码学操作和许可证格式化逻辑,对外提供简单易用的API。 + * + *

主要功能: + *

+ * + *

激活码格式: + * {@code 许可证ID-许可证内容Base64-数字签名Base64-证书Base64} + * + *

安全特性: + *

+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "授权上下文") +@NoArgsConstructor(access = AccessLevel.PRIVATE) // 防止实例化 +public class LicenseContextHolder { + + /** + * 生成JetBrains产品激活码 + * + *

此方法是整个类的核心方法,负责生成符合JetBrains规范的激活码。 + * 生成的激活码包含了所有必要的许可证信息,并使用RSA数字签名确保安全性。 + * + *

生成过程: + *

    + *
  1. 生成唯一的许可证ID
  2. + *
  3. 构建包含产品信息的许可证对象
  4. + *
  5. 将许可证对象转换为JSON并编码为Base64
  6. + *
  7. 使用RSA私钥对许可证内容进行数字签名
  8. + *
  9. 获取X.509证书并编码为Base64
  10. + *
  11. 按照指定格式组装最终的激活码
  12. + *
+ * + *

激活码格式:{@code 许可证ID-许可证内容Base64-数字签名Base64-证书Base64} + * + * @param licensesName 许可证名称,通常为公司或组织名称 + * @param assigneeName 被授权人名称,即许可证的使用者 + * @param expiryDate 过期日期,格式为 yyyy-MM-dd + * @param productCodeSet 产品代码集合,包含所有需要激活的产品 + * @return 符合JetBrains规范的激活码字符串 + * @throws IllegalArgumentException 当密码学操作或证书处理失败时抛出 + */ + public static String generateLicense(String licensesName, String assigneeName, String expiryDate, + Set productCodeSet) { + log.info("开始生成许可证 - 许可证名称: {}, 被授权人: {}, 过期日期: {}, 产品数量: {}", + licensesName, assigneeName, expiryDate, productCodeSet.size()); + + // 1. 生成唯一的许可证ID + String licenseId = IdUtil.fastSimpleUUID(); + log.debug("生成许可证ID: {}", licenseId); + + // 2. 构建产品列表,为每个产品设置相同的过期日期 + List products = productCodeSet.stream() + .map(productCode -> new Product() + .setCode(productCode) // 产品代码 + .setFallbackDate(expiryDate) // 备用过期日期 + .setPaidUpTo(expiryDate)) // 付费至日期 + .collect(Collectors.toList()); + + // 3. 构建许可证主体对象 + LicensePart licensePart = new LicensePart() + .setLicenseId(licenseId) // 许可证ID + .setLicenseeName(licensesName) // 许可证名称 + .setAssigneeName(assigneeName) // 被授权人 + .setProducts(products); // 产品列表 + + // 4. 将许可证对象转换为JSON并进行Base64编码 + String licensePartJson = JSONUtil.toJsonStr(licensePart); + String licensePartBase64 = Base64.encode(licensePartJson); + log.debug("许可证JSON内容长度: {} 字符", licensePartJson.length()); + + // 5. 加载密码学组件:私钥、公钥和证书 + PrivateKey privateKey = PemUtil.readPemPrivateKey(IoUtil.toStream(CertificateContextHolder.privateKeyFile())); + PublicKey publicKey = PemUtil.readPemPublicKey(IoUtil.toStream(CertificateContextHolder.publicKeyFile())); + Certificate certificate = SecureUtil.readX509Certificate(IoUtil.toStream(CertificateContextHolder.codeCrtFile())); + + // 6. 使用SHA1withRSA算法进行数字签名 + Sign sign = SignUtil.sign(SHA1withRSA, privateKey.getEncoded(), publicKey.getEncoded()); + String signatureBase64 = Base64.encode(sign.sign(licensePartJson)); + + // 7. 将X.509证书编码为Base64 + String certBase64; + try { + certBase64 = Base64.encode(certificate.getEncoded()); + } catch (CertificateEncodingException e) { + log.error("证书编码失败", e); + throw new IllegalArgumentException("证书编码异常", e); + } + + // 8. 按照JetBrains格式组装最终的激活码 + String activationCode = CharSequenceUtil.format("{}-{}-{}-{}", + licenseId, licensePartBase64, signatureBase64, certBase64); + + log.info("许可证生成成功 - 激活码长度: {} 字符", activationCode.length()); + + return activationCode; + } + + // ==================== 内部数据类 ==================== + + /** + * 许可证主体信息实体类 + * + *

封装了许可证的所有基本信息,包括许可证ID、名称、被授权人和产品列表。 + * 此类的实例将被序列化为JSON格式,作为激活码的一部分。 + * + *

字段说明: + *

+ */ + @Data + @Accessors(chain = true) // 支持链式调用 + public static class LicensePart { + + /** 许可证唯一标识符,由UUID生成 */ + private String licenseId; + + /** 许可证名称,通常为公司或组织名称 */ + private String licenseeName; + + /** 被授权人名称,即许可证的使用者 */ + private String assigneeName; + + /** 包含在许可证中的产品列表 */ + private List products; + + /** + * 元数据信息,默认值说明: + * - 01: 版本标识 + * - 20230914: 日期标识 + * - PSAX000005: 产品类型和序列号 + */ + private String metadata = "0120230914PSAX000005"; + } + + /** + * 产品信息实体类 + * + *

封装了单个产品在许可证中的所有相关信息。 + * 每个产品都包含产品代码、备用过期日期和付费截止日期。 + * + *

字段说明: + *

+ * + *

常见产品代码: + *

+ */ + @Data + @Accessors(chain = true) // 支持链式调用 + public static class Product { + + /** 产品代码,用于标识不同的JetBrains产品 */ + private String code; + + /** 备用过期日期,格式为 yyyy-MM-dd */ + private String fallbackDate; + + /** 付费截止日期,格式为 yyyy-MM-dd */ + private String paidUpTo; + } + +} diff --git a/src/main/java/com/qiumo/help/context/PluginsContextHolder.java b/src/main/java/com/qiumo/help/context/PluginsContextHolder.java new file mode 100644 index 0000000..540346c --- /dev/null +++ b/src/main/java/com/qiumo/help/context/PluginsContextHolder.java @@ -0,0 +1,347 @@ +package com.qiumo.help.context; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import com.qiumo.help.util.FileTools; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * JetBrains插件信息上下文管理器 + * + *

此类负责管理JetBrains插件市场中付费插件的信息,包括插件ID、名称、产品代码、定价模式等。 + * 插件信息通过JetBrains官方API实时获取,并缓存到本地JSON文件中,用于许可证生成时的插件选择。 + * + *

主要功能: + *

+ * + *

数据流程: + *

    + *
  1. 从本地JSON文件加载已缓存的插件信息
  2. + *
  3. 定期调用JetBrains API获取最新插件列表
  4. + *
  5. 过滤掉免费插件,只保留付费插件
  6. + *
  7. 获取每个插件的详细信息(特别是产品代码)
  8. + *
  9. 更新本地缓存文件
  10. + *
+ * + *

API端点: + *

+ * + *

缓存策略: + *

+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "插件上下文") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PluginsContextHolder { + + // ==================== 常量定义 ==================== + + /** JetBrains插件市场基础URL */ + private static final String PLUGIN_BASIC_URL = "https://plugins.jetbrains.com"; + + /** 插件列表API地址,获取所有插件基本信息 */ + private static final String PLUGIN_LIST_URL = + PLUGIN_BASIC_URL + "/api/searchPlugins?max=10000&offset=0&orderBy=name"; + + /** 插件详情API地址模板,用于获取单个插件的详细信息 */ + private static final String PLUGIN_INFO_URL = PLUGIN_BASIC_URL + "/api/plugins/"; + + /** 插件信息缓存文件路径 */ + private static final String PLUGIN_JSON_FILE_NAME = "external/data/plugin.json"; + + // ==================== 静态字段 ==================== + + /** 插件信息缓存列表,存储所有已加载的付费插件信息 */ + private static List pluginCacheList; + + /** 插件JSON缓存文件对象 */ + private static File pluginsJsonFile; + + // ==================== 核心方法 ==================== + + /** + * 初始化插件上下文 + * + *

此方法负责初始化插件信息管理系统,包括加载本地缓存和启动刷新任务。 + * 如果本地缓存文件不存在或格式错误,会自动从网络获取最新数据。 + * + *

初始化过程: + *

    + *
  1. 获取或创建插件缓存JSON文件
  2. + *
  3. 读取并验证JSON文件内容
  4. + *
  5. 如果数据有效,解析为对象列表
  6. + *
  7. 如果数据无效,初始化为空列表
  8. + *
  9. 启动异步刷新任务获取最新数据
  10. + *
+ * + *

注意事项: + *

    + *
  • 此方法应在应用启动时调用,且只调用一次
  • + *
  • 网络刷新任务是异步执行的,不会阻塞应用启动
  • + *
  • 刷新失败不会影响应用正常运行
  • + *
+ * + * @throws IllegalArgumentException 当文件读取失败时抛出 + */ + public static void init() { + log.info("开始初始化插件上下文..."); + + // 获取或创建插件缓存文件 + pluginsJsonFile = FileTools.getFileOrCreat(PLUGIN_JSON_FILE_NAME); + log.debug("插件缓存文件路径: {}", pluginsJsonFile.getAbsolutePath()); + + // 读取并验证JSON文件内容 + String pluginJsonArray; + try { + pluginJsonArray = IoUtil.readUtf8(FileUtil.getInputStream(pluginsJsonFile)); + } catch (IORuntimeException e) { + throw new IllegalArgumentException( + CharSequenceUtil.format("{} 文件读取失败!", PLUGIN_JSON_FILE_NAME), e); + } + + // 检查JSON数据的有效性 + if (CharSequenceUtil.isBlank(pluginJsonArray) || !JSONUtil.isTypeJSON(pluginJsonArray)) { + log.warn("插件缓存文件为空或格式错误,初始化为空列表"); + pluginCacheList = new ArrayList<>(); + // 启动异步刷新任务获取数据 + refreshJsonFile(); + } else { + // 解析JSON数据为插件对象列表 + pluginCacheList = JSONUtil.toList(pluginJsonArray, PluginCache.class); + log.info("插件上下文初始化成功!加载插件数量: {}", pluginCacheList.size()); + + // 启动异步刷新任务获取最新数据 + refreshJsonFile(); + } + } + + /** + * 获取插件信息缓存列表 + * + *

返回已加载的所有JetBrains付费插件信息列表。 + * 该列表包含插件ID、名称、产品代码、定价模式和图标等信息。 + * + *

使用场景: + *

    + *
  • 许可证生成时选择插件产品代码
  • + *
  • 前端界面显示插件列表
  • + *
  • 插件管理和统计功能
  • + *
+ * + *

注意事项: + *

    + *
  • 返回的是实际的列表引用,修改会影响缓存
  • + *
  • 列表中只包含付费插件,免费插件已被过滤
  • + *
  • 需要在 {@link #init()} 方法调用后使用
  • + *
+ * + * @return 插件信息缓存列表,不为null + */ + public static List pluginCacheList() { + return PluginsContextHolder.pluginCacheList; + } + + public static void refreshJsonFile() { + log.info("从'JetBrains.com'刷新中..."); + CompletableFuture + .supplyAsync(PluginsContextHolder::pluginList) + .thenApply(PluginsContextHolder::pluginListFilter) + .thenApply(PluginsContextHolder::pluginConversion) + .thenAccept(PluginsContextHolder::overrideJsonFile) + .thenRun(() -> log.info("刷新成功!")) + .exceptionally(throwable -> { + log.error("刷新失败!", throwable); + return null; + }); + } + + public static void overrideJsonFile(List pluginCaches) { + log.info("源大小 => [{}], 新增大小 => [{}]", pluginCacheList.size(), pluginCaches.size()); + pluginCacheList.addAll(pluginCaches); + String jsonStr = JSONUtil.toJsonStr(pluginCacheList); + try { + FileUtil.writeString(JSONUtil.formatJsonStr(jsonStr), pluginsJsonFile, StandardCharsets.UTF_8); + log.info("Json文件已覆写!"); + } catch (IORuntimeException e) { + throw new IllegalArgumentException(CharSequenceUtil.format("{} 文件写入失败!", PLUGIN_JSON_FILE_NAME), e); + } + + } + + public static PluginList pluginList() { + return HttpUtil.createGet(PLUGIN_LIST_URL) + .thenFunction(response -> { + try (InputStream is = response.bodyStream()) { + if (!response.isOk()) { + throw new IllegalArgumentException( + CharSequenceUtil.format("{} 请求失败! = {}", PLUGIN_LIST_URL, response)); + } + PluginList pluginList = JSONUtil.toBean(IoUtil.readUtf8(is), PluginList.class); + log.info("获取大小 => [{}]", pluginList.getTotal()); + return pluginList; + } catch (IOException e) { + throw new IllegalArgumentException(CharSequenceUtil.format("{} 请求IO读取失败!", PLUGIN_LIST_URL), + e); + } + }); + } + + public static List pluginListFilter(PluginList pluginList) { + List plugins = pluginList.getPlugins() + .stream() + .filter(plugin -> !PluginsContextHolder.pluginCacheList.contains(new PluginCache().setId(plugin.getId()))) + .filter(plugin -> !CharSequenceUtil.equals(plugin.getPricingModel(), "FREE")) + .collect(Collectors.toList()); + log.info("过滤后大小 => [{}]", plugins.size()); + return plugins; + } + + public static List pluginConversion(List pluginList) { + List list = pluginList + .stream() + .parallel() + .map(plugin -> { + String productCode = pluginInfo(plugin).getPurchaseInfo().getProductCode(); + return new PluginCache() + .setId(plugin.getId()) + .setProductCode(productCode) + .setName(plugin.getName()) + .setPricingModel(plugin.getPricingModel()) + .setIcon(StrUtil.isNotBlank(plugin.getIcon()) ? PLUGIN_BASIC_URL + plugin.getIcon() : null) + ; + }) + .collect(Collectors.toList()); + log.info("转换后大小 => [{}]", list.size()); + return list; + } + + public static PluginInfo pluginInfo(PluginList.Plugin plugin) { + return HttpUtil.createGet(PLUGIN_INFO_URL + plugin.getId()) + .thenFunction(response -> { + try (InputStream is = response.bodyStream()) { + if (!response.isOk()) { + throw new IllegalArgumentException( + CharSequenceUtil.format("{} 请求失败! = {}", PLUGIN_INFO_URL, response)); + } + PluginInfo pluginInfo = JSONUtil.toBean(IoUtil.readUtf8(is), PluginInfo.class); + log.info("已抓取 => ID = [{}], 名称 = [{}], Code = [{}]", pluginInfo.getId(), plugin.getName(), + pluginInfo.getPurchaseInfo().getProductCode()); + return pluginInfo; + } catch (IOException e) { + throw new IllegalArgumentException(CharSequenceUtil.format("{} 请求IO读取失败!", PLUGIN_LIST_URL), + e); + } + }); + } + + + @Data + @Accessors(chain = true) + public static class PluginCache { + + private Long id; + private String productCode; + private String name; + private String pricingModel; + private String icon; + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PluginCache)) { + return false; + } + + return id.equals(((PluginCache) o).id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + } + + @Data + @Accessors(chain = true) + public static class PluginInfo { + + private Long id; + + private PurchaseInfo purchaseInfo; + + @Data + @Accessors(chain = true) + public static class PurchaseInfo { + + private String productCode; + } + } + + @Data + @Accessors(chain = true) + public static class PluginList { + + private List plugins; + private Long total; + + + @Data + @Accessors(chain = true) + public static class Plugin { + + private Long id; + private String name; + private String preview; + private Integer downloads; + private String pricingModel; + private String organization; + private String icon; + private String previewImage; + private Double rating; + private VendorInfo vendorInfo; + } + + @Data + @Accessors(chain = true) + public static class VendorInfo { + + private String name; + private Boolean isVerified; + } + } +} diff --git a/src/main/java/com/qiumo/help/context/ProductsContextHolder.java b/src/main/java/com/qiumo/help/context/ProductsContextHolder.java new file mode 100644 index 0000000..c9ba9b6 --- /dev/null +++ b/src/main/java/com/qiumo/help/context/ProductsContextHolder.java @@ -0,0 +1,179 @@ +package com.qiumo.help.context; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.json.JSONUtil; +import com.qiumo.help.util.FileTools; +import java.util.ArrayList; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.util.List; + +/** + * JetBrains产品信息上下文管理器 + * + *

此类负责管理JetBrains所有IDE产品的信息,包括产品名称、产品代码和图标等。 + * 产品信息从JSON配置文件中加载,用于许可证生成时的产品选择和识别。 + * + *

主要功能: + *

    + *
  • 从JSON文件加载JetBrains产品信息
  • + *
  • 提供产品信息的缓存和访问接口
  • + *
  • 支持产品代码到产品名称的映射
  • + *
  • 为许可证生成提供产品代码列表
  • + *
+ * + *

支持的产品包括: + *

    + *
  • IntelliJ IDEA Ultimate (II)
  • + *
  • PhpStorm (PS)
  • + *
  • WebStorm (WS)
  • + *
  • PyCharm Professional (PY)
  • + *
  • RubyMine (RM)
  • + *
  • CLion (CL)
  • + *
  • DataGrip (DB)
  • + *
  • GoLand (GO)
  • + *
  • Rider (RD)
  • + *
  • AppCode (AC)
  • + *
  • 以及其他JetBrains系列产品
  • + *
+ * + *

数据来源: + * 产品信息可以通过JetBrains官方API获取: + * {@code https://data.services.jetbrains.com/products?fields=name,salesCode} + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "产品上下文") +@NoArgsConstructor(access = AccessLevel.PRIVATE) // 防止实例化 +public class ProductsContextHolder { + + // ==================== 常量定义 ==================== + + /** 产品信息配置文件路径 */ + private static final String PRODUCT_JSON_FILE_NAME = "external/data/product.json"; + + // ==================== 静态字段 ==================== + + /** 产品信息缓存列表,存储所有加载的产品信息 */ + private static List productCacheList; + + // TODO 通过该接口可以获取付费IDE的CODE + // TODO https://data.services.jetbrains.com/products?fields=name,salesCode + + public static void init() { + log.info("开始初始化产品上下文..."); + + // 获取或创建产品配置文件 + File productJsonFile = FileTools.getFileOrCreat(PRODUCT_JSON_FILE_NAME); + log.debug("产品配置文件路径: {}", productJsonFile.getAbsolutePath()); + + // 读取JSON文件内容 + String productJsonArray; + try { + productJsonArray = IoUtil.readUtf8(FileUtil.getInputStream(productJsonFile)); + log.debug("成功读取产品配置文件,内容长度: {} 字符", productJsonArray.length()); + } catch (IORuntimeException e) { + throw new IllegalArgumentException( + CharSequenceUtil.format("{} 文件读取失败!", PRODUCT_JSON_FILE_NAME), e); + } + + // 验证JSON文件内容并解析 + if (CharSequenceUtil.isBlank(productJsonArray) || !JSONUtil.isTypeJSON(productJsonArray)) { + log.error("产品数据不存在或格式错误!配置文件: {}", PRODUCT_JSON_FILE_NAME); + // 初始化为空列表,避免空指针异常 + productCacheList = new ArrayList<>(); + } else { + // 解析JSON数据为产品对象列表 + productCacheList = JSONUtil.toList(productJsonArray, ProductCache.class); + log.info("产品上下文初始化成功!加载产品数量: {}", productCacheList.size()); + + // 输出加载的产品信息(调试级别) + if (log.isDebugEnabled()) { + productCacheList.forEach(product -> + log.debug("加载产品: {} ({})", product.getName(), product.getProductCode())); + } + } + } + + /** + * 获取产品信息缓存列表 + * + *

返回已加载的所有JetBrains产品信息列表。 + * 该列表包含产品名称、产品代码和图标等信息。 + * + *

使用场景: + *

    + *
  • 许可证生成时选择产品代码
  • + *
  • 前端界面显示产品列表
  • + *
  • 产品代码到产品名称的映射
  • + *
+ * + *

注意事项: + *

    + *
  • 返回的是不可变列表,不能直接修改
  • + *
  • 如果初始化失败,可能返回空列表
  • + *
  • 需要在 {@link #init()} 方法调用后使用
  • + *
+ * + * @return 产品信息缓存列表,不为null + */ + public static List productCacheList() { + return ProductsContextHolder.productCacheList; + } + + // ==================== 内部数据类 ==================== + + /** + * 产品信息缓存实体类 + * + *

封装了单个JetBrains产品的基本信息,包括产品名称、产品代码和图标类名。 + * 该类的实例从 JSON 配置文件中反序列化得到。 + * + *

字段说明: + *

    + *
  • name - 产品显示名称,如 "IntelliJ IDEA Ultimate"
  • + *
  • productCode - 产品代码,如 "II" 或用逗号分隔的多个代码
  • + *
  • iconClass - CSS图标类名,用于前端界面显示
  • + *
+ * + *

产品代码示例: + *

    + *
  • "II" - IntelliJ IDEA Ultimate
  • + *
  • "PS" - PhpStorm
  • + *
  • "WS" - WebStorm
  • + *
  • "PY" - PyCharm Professional
  • + *
  • "RM" - RubyMine
  • + *
  • "CL" - CLion
  • + *
  • "DB" - DataGrip
  • + *
  • "GO" - GoLand
  • + *
  • "RD" - Rider
  • + *
  • "AC" - AppCode
  • + *
+ * + *

数据来源: + * 可以通过JetBrains官方API获取最新产品信息: + * {@code https://data.services.jetbrains.com/products?fields=name,salesCode} + */ + @Data + public static class ProductCache { + + /** 产品显示名称,如 "IntelliJ IDEA Ultimate" */ + private String name; + + /** 产品代码,单个或多个用逗号分隔,如 "II" 或 "II,IC" */ + private String productCode; + + /** CSS图标类名,用于前端界面显示产品图标 */ + private String iconClass; + } +} diff --git a/src/main/java/com/qiumo/help/controller/DataController.java b/src/main/java/com/qiumo/help/controller/DataController.java new file mode 100644 index 0000000..267bb5d --- /dev/null +++ b/src/main/java/com/qiumo/help/controller/DataController.java @@ -0,0 +1,222 @@ +package com.qiumo.help.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +import com.qiumo.help.context.LicenseContextHolder; +import com.qiumo.help.context.PluginsContextHolder; +import com.qiumo.help.context.ProductsContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 数据接口控制器 + * + *

此控制器提供前端所需的基础数据API,包括产品列表、插件列表等。 + * 这些数据用于前端界面的动态展示和用户选择。 + * + *

主要功能: + *

    + *
  • 提供JetBrains产品列表数据
  • + *
  • 提供付费插件列表数据
  • + *
  • 支持前端动态加载数据
  • + *
  • 返回JSON格式的结构化数据
  • + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ +@Slf4j(topic = "数据接口") +@RestController +@RequestMapping("/api") +public class DataController { + + /** + * 获取JetBrains产品列表 + * + *

返回所有支持的JetBrains IDE产品信息,包括产品名称、产品代码和描述等。 + * 前端可以使用这些数据构建产品选择下拉框。 + * + *

返回的产品信息包含: + *

    + *
  • name - 产品显示名称
  • + *
  • productCode - 产品代码(用于许可证生成)
  • + *
  • iconClass - 图标CSS类名
  • + *
+ * + *

请求示例: + *

+     * GET /api/products
+     * 
+ * + *

响应示例: + *

+     * [
+     *   {
+     *     "name": "IntelliJ IDEA Ultimate",
+     *     "productCode": "II",
+     *     "iconClass": "icon-idea"
+     *   },
+     *   {
+     *     "name": "PhpStorm",
+     *     "productCode": "PS", 
+     *     "iconClass": "icon-phpstorm"
+     *   }
+     * ]
+     * 
+ * + * @return JetBrains产品信息列表 + */ + @GetMapping("/products") + public List getProducts() { + log.debug("获取产品列表,产品数量: {}", ProductsContextHolder.productCacheList().size()); + return ProductsContextHolder.productCacheList(); + } + + /** + * 获取JetBrains付费插件列表 + * + *

返回所有支持的JetBrains付费插件信息,包括插件名称、ID和产品代码等。 + * 前端可以使用这些数据构建插件选择下拉框。 + * + *

返回的插件信息包含: + *

    + *
  • id - 插件唯一标识符
  • + *
  • name - 插件显示名称
  • + *
  • productCode - 产品代码(用于许可证生成)
  • + *
  • pricingModel - 定价模式
  • + *
  • icon - 插件图标URL
  • + *
+ * + *

请求示例: + *

+     * GET /api/plugins
+     * 
+ * + *

响应示例: + *

+     * [
+     *   {
+     *     "id": 7973,
+     *     "name": "SonarLint",
+     *     "productCode": "SONAR_LINT",
+     *     "pricingModel": "PAID",
+     *     "icon": "https://plugins.jetbrains.com/files/7973/icon.svg"
+     *   }
+     * ]
+     * 
+ * + * @return JetBrains付费插件信息列表 + */ + @GetMapping("/plugins") + public List getPlugins() { + log.debug("获取插件列表,插件数量: {}", PluginsContextHolder.pluginCacheList().size()); + return PluginsContextHolder.pluginCacheList(); + } + + /** + * 生成JetBrains产品激活码 + * + * @param productCode 产品代码 + * @param licenseeName 许可证名称 + * @param assigneeName 被授权人名称 + * @param expiryDate 过期日期 + * @return 激活码字符串 + */ + @GetMapping("/generateLicense") + public String generateLicense( + @RequestParam(required = false) String productCode, + @RequestParam String licenseeName, + @RequestParam String assigneeName, + @RequestParam String expiryDate) { + + Set productCodeSet; + + if (CharSequenceUtil.isBlank(productCode)) { + // 未指定产品代码,自动包含所有可用产品 + List productCodeList = ProductsContextHolder.productCacheList() + .stream() + .map(ProductsContextHolder.ProductCache::getProductCode) + .filter(StrUtil::isNotBlank) + .map(code -> CharSequenceUtil.splitTrim(code, ",")) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + List pluginCodeList = PluginsContextHolder.pluginCacheList() + .stream() + .map(PluginsContextHolder.PluginCache::getProductCode) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + + productCodeSet = CollUtil.newHashSet(productCodeList); + productCodeSet.addAll(pluginCodeList); + } else { + productCodeSet = CollUtil.newHashSet(CharSequenceUtil.splitTrim(productCode, ',')); + } + + return LicenseContextHolder.generateLicense( + licenseeName, + assigneeName, + expiryDate, + productCodeSet + ); + } + + /** + * 生成JetBrains插件激活码 + * + * @param pluginId 插件ID + * @param licenseeName 许可证名称 + * @param assigneeName 被授权人名称 + * @param expiryDate 过期日期 + * @return 插件激活码字符串 + */ + @GetMapping("/generatePluginLicense") + public String generatePluginLicense( + @RequestParam String pluginId, + @RequestParam String licenseeName, + @RequestParam String assigneeName, + @RequestParam String expiryDate) { + + String productCode = PluginsContextHolder.pluginCacheList() + .stream() + .filter(plugin -> Objects.equals(plugin.getId(), pluginId)) + .map(PluginsContextHolder.PluginCache::getProductCode) + .filter(StrUtil::isNotBlank) + .findFirst() + .orElse(""); + + if (CharSequenceUtil.isBlank(productCode)) { + productCode = "PLUGIN_" + pluginId; + } + + Set productCodeSet = CollUtil.newHashSet(productCode); + + return LicenseContextHolder.generateLicense( + licenseeName, + assigneeName, + expiryDate, + productCodeSet + ); + } + + /** + * 下载ja-netfilter代理工具 + */ + @GetMapping("/downloadAgent") + public void downloadAgent(HttpServletResponse response) throws IOException { + response.sendRedirect("/api/downloadZip"); + } +} diff --git a/src/main/java/com/qiumo/help/controller/JRebelController.java b/src/main/java/com/qiumo/help/controller/JRebelController.java new file mode 100644 index 0000000..38de713 --- /dev/null +++ b/src/main/java/com/qiumo/help/controller/JRebelController.java @@ -0,0 +1,54 @@ +package com.qiumo.help.controller; + +import com.qiumo.help.service.JrebelService; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j(topic = "JRebel激活控制器") +@RestController +@RequiredArgsConstructor +public class JRebelController { + + private final JrebelService jrebelService; + + @SneakyThrows + @RequestMapping("/guid") + public void guid(HttpServletRequest request, HttpServletResponse response) { + response.setContentType("text/html; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + String body = UUID.randomUUID().toString(); + response.getWriter().print(body); + } + + @RequestMapping({"/jrebel/leases"}) + public void jrebelLeases(HttpServletRequest request, HttpServletResponse response) { + jrebelService.jrebelLeasesHandler(request, response); + } + + @RequestMapping("/jrebel/leases/1") + public void jrebelLeases1(HttpServletRequest request, HttpServletResponse response) { + jrebelService.jrebelLeases1Handler(request, response); + } + + @RequestMapping("/agent/leases") + public void agentLeases(HttpServletRequest request, HttpServletResponse response) { + jrebelService.jrebelLeasesHandler(request, response); + } + + @RequestMapping("/agent/leases/1") + public void agentLeases1(HttpServletRequest request, HttpServletResponse response) { + jrebelService.jrebelLeases1Handler(request, response); + } + + @RequestMapping("/jrebel/validate-connection") + public void jrebelValidateHandler(HttpServletRequest request, HttpServletResponse response) { + jrebelService.jrebelValidateHandler(request, response); + } + +} diff --git a/src/main/java/com/qiumo/help/controller/LicenseCodeController.java b/src/main/java/com/qiumo/help/controller/LicenseCodeController.java new file mode 100644 index 0000000..7753e9d --- /dev/null +++ b/src/main/java/com/qiumo/help/controller/LicenseCodeController.java @@ -0,0 +1,228 @@ +package com.qiumo.help.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +import com.qiumo.help.context.LicenseContextHolder; +import com.qiumo.help.context.PluginsContextHolder; +import com.qiumo.help.context.ProductsContextHolder; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 许可证代码生成控制器 + * + *

此控制器专门用于生成JetBrains产品的激活码(许可证代码)。 + * 它能够根据用户的输入参数生成适用于不同产品的激活码。 + * + *

主要功能: + *

    + *
  • 生成个人或企业版许可证
  • + *
  • 支持指定产品代码或自动包含所有产品
  • + *
  • 自定义许可证名称、被授权人和过期日期
  • + *
  • 支持JetBrains所有付费IDE和插件
  • + *
+ * + *

生成的激活码格式为: + * {@code 许可证ID-许可证内容Base64-签名Base64-证书Base64} + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@RestController +@RequestMapping("/license-code") +public class LicenseCodeController { + + /** + * 生成许可证请求参数实体类 + * + *

封装了生成许可证所需的全部参数,包括许可证基本信息和产品代码。 + * + *

参数说明: + *

    + *
  • licenseName - 许可证名称,用于标识许可证的来源(如公司名称或个人名称)
  • + *
  • assigneeName - 被授权人名称,即许可证的使用者
  • + *
  • expiryDate - 过期日期,格式为 yyyy-MM-dd(如:2025-12-31)
  • + *
  • productCode - 产品代码,多个代码用逗号分隔,为空时包含所有产品
  • + *
+ * + *

使用示例: + *

+     * {
+     *   "licenseName": "QiuMo Technology",
+     *   "assigneeName": "张三",
+     *   "expiryDate": "2025-12-31",
+     *   "productCode": "II,PS,WS,RM,PCC,PC,CLN"
+     * }
+     * 
+ */ + @Data + public static class GenerateLicenseReqBody { + + /** 许可证名称(公司或组织名称) */ + private String licenseName; + + /** 被授权人名称(使用者名称) */ + private String assigneeName; + + /** 过期日期(格式:yyyy-MM-dd) */ + private String expiryDate; + + /** 产品代码(多个代码用逗号分隔,为空时包含所有产品) */ + private String productCode; + } + + /** + * 生成JetBrains产品激活码接口(GET方式) + * + *

此接口提供GET方式访问,用于前端页面直接调用生成激活码。 + * + * @param productCode 产品代码 + * @param licenseeName 许可证名称 + * @param assigneeName 被授权人名称 + * @param expiryDate 过期日期 + * @return JetBrains产品激活码字符串 + */ + @GetMapping("/generate") + public String generateLicenseByGet( + @RequestParam(required = false) String productCode, + @RequestParam String licenseeName, + @RequestParam String assigneeName, + @RequestParam String expiryDate) { + + GenerateLicenseReqBody body = new GenerateLicenseReqBody(); + body.setProductCode(productCode); + body.setLicenseName(licenseeName); + body.setAssigneeName(assigneeName); + body.setExpiryDate(expiryDate); + + return generateLicense(body); + } + + + /** + * 生成JetBrains产品激活码接口 + * + *

此接口用于生成JetBrains系列产品的激活码(许可证代码)。 + * 生成的激活码可以用于激活各种JetBrains IDE和插件。 + * + *

功能特点: + *

    + *
  • 智能产品选择:如果未指定产品代码,自动包含所有可用产品
  • + *
  • 灵活的过期设置:支持自定义过期日期
  • + *
  • 完整产品支持:包括IDE和付费插件
  • + *
  • RSA数字签名:确保激活码的安全性和完整性
  • + *
+ * + *

生成过程: + *

    + *
  1. 根据请求参数解析产品代码集合
  2. + *
  3. 构建许可证对象,包含产品信息、有效期等
  4. + *
  5. 使用RSA私钥对许可证内容进行数字签名
  6. + *
  7. 组装最终的激活码字符串
  8. + *
+ * + *

请求示例: + *

+     * POST /generateLicense
+     * Content-Type: application/json
+     *
+     * {
+     *   "licenseName": "QiuMo Technology",
+     *   "assigneeName": "张三",
+     *   "expiryDate": "2025-12-31",
+     *   "productCode": "II,PS,WS"
+     * }
+     * 
+ * + *

响应示例: + *

+     * K9V7I1-FLS6QH-eyJsaWNlbnNlSWQiOi...(完整激活码)
+     * 
+ * + * @param body 生成许可证的请求参数 + * @return JetBrains产品激活码字符串 + */ + @PostMapping("/generate") + public String generateLicense(@RequestBody GenerateLicenseReqBody body) { + // 定义产品代码集合,用于存储所有需要包含在许可证中的产品代码 + Set productCodeSet; + + // 判断是否指定了产品代码 + if (CharSequenceUtil.isBlank(body.getProductCode())) { + // 未指定产品代码,自动包含所有可用产品 + + // 获取所有JetBrains IDE产品代码 + List productCodeList = ProductsContextHolder.productCacheList() + .stream() + .map(ProductsContextHolder.ProductCache::getProductCode) // 提取产品代码 + .filter(StrUtil::isNotBlank) // 过滤空值 + .map(productCode -> CharSequenceUtil.splitTrim(productCode, ",")) // 按逗号分割 + .flatMap(Collection::stream) // 展平成一维数据流 + .collect(Collectors.toList()); + + // 获取所有付费插件代码 + List pluginCodeList = PluginsContextHolder.pluginCacheList() + .stream() + .map(PluginsContextHolder.PluginCache::getProductCode) // 提取插件产品代码 + .filter(StrUtil::isNotBlank) // 过滤空值 + .collect(Collectors.toList()); + + // 合并IDE产品代码和插件代码,去除重复 + productCodeSet = CollUtil.newHashSet(productCodeList); + productCodeSet.addAll(pluginCodeList); + + } else { + // 已指定产品代码,解析用户输入的产品代码列表 + productCodeSet = CollUtil.newHashSet(CharSequenceUtil.splitTrim(body.getProductCode(), ',')); + } + + // 调用许可证生成服务,生成最终的激活码 + return LicenseContextHolder.generateLicense( + body.getLicenseName(), // 许可证名称 + body.getAssigneeName(), // 被授权人名称 + body.getExpiryDate(), // 过期日期 + productCodeSet // 产品代码集合 + ); + } + + /** + * 插件激活码生成请求体类 + * + *

此内部类用于封装生成JetBrains插件激活码时的请求参数。 + * 专门针对付费插件的激活需求设计。 + * + * @since 1.0.0 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class GeneratePluginLicenseReqBody { + /** 许可证名称(组织名称或公司名称) */ + private String licenseeName; + + /** 被授权人名称(使用者名称) */ + private String assigneeName; + + /** 过期日期(格式:yyyy-MM-dd) */ + private String expiryDate; + + /** 插件ID */ + private String pluginId; + } + +} diff --git a/src/main/java/com/qiumo/help/controller/LicenseServerController.java b/src/main/java/com/qiumo/help/controller/LicenseServerController.java new file mode 100644 index 0000000..ca0c8e4 --- /dev/null +++ b/src/main/java/com/qiumo/help/controller/LicenseServerController.java @@ -0,0 +1,233 @@ +package com.qiumo.help.controller; + +import cn.hutool.core.util.RandomUtil; +import com.qiumo.help.model.ObtainTicketResponse; +import com.qiumo.help.model.PingResponse; +import com.qiumo.help.model.ProlongTicketResponse; +import com.qiumo.help.model.ReleaseTicketResponse; +import com.qiumo.help.util.LicenseServerUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * 许可证服务器控制器 + * + *

此控制器模拟JetBrains官方许可证服务器的行为,提供以下主要功能: + *

    + *
  • 获取许可证凭证(obtainTicket)
  • + *
  • 检查服务器连通性(ping)
  • + *
  • 延长许可证有效期(prolongTicket)
  • + *
  • 释放许可证凭证(releaseTicket)
  • + *
+ * + *

所有的接口都返回XML格式的数据,并使用RSA签名保证数据的安全性。 + * + *

路径说明: + * 所有的请求路径都以 {@code /rpc/} 开头,这是为了兼容JetBrains官方许可证服务器的API设计。 + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "许可证服务器") // 使用自定义日志主题 +@RestController +@RequestMapping(value = "/rpc", produces = "text/xml; charset=UTF-8") // XML输出格式,兼容JetBrains协议 +public class LicenseServerController { + + /** + * 获取许可证凭证接口 + * + *

这是JetBrains产品首次连接许可证服务器时调用的接口。 + * 该接口会生成一个许可证凭证(ticket),并返回相关的许可证信息。 + * + *

请求参数说明: + *

    + *
  • hostName - 客户端主机名,用于标识许可证使用者
  • + *
  • machineId - 客户端机器唯一标识符,用于硬件绑定
  • + *
  • salt - 加密盐值,用于增强通信安全性
  • + *
+ * + *

返回内容包括: + *

    + *
  • ticketId - 凭证唯一标识符
  • + *
  • ticketProperties - 凭证属性,包含许可证类型和有效期信息
  • + *
  • serverLease - 服务器租约信息
  • + *
  • confirmationStamp - 确认时间戳,用于验证请求的有效性
  • + *
+ * + * @param request HTTP请求对象,包含客户端参数 + * @return 签名后的XML响应数据 + */ + @RequestMapping("/obtainTicket.action") + public Object obtainTicket(HttpServletRequest request) { + // 提取请求参数 + String hostName = request.getParameter("hostName"); + String machineId = request.getParameter("machineId"); + String salt = request.getParameter("salt"); + + log.debug("接收到获取许可证凭证请求 - 主机: {}, 机器ID: {}", hostName, machineId); + + // 构建响应对象 + ObtainTicketResponse response = new ObtainTicketResponse(); + response.setAction("NONE"); // 无需额外操作 + response.setConfirmationStamp(LicenseServerUtils.getConfirmationStamp(machineId)); // 生成确认时间戳 + response.setLeaseSignature(LicenseServerUtils.getLeaseSignature()); // 生成租约签名 + response.setMessage(""); // 无错误消息 + response.setProlongationPeriod("600000"); // 延长周期(毫秒) + response.setResponseCode("OK"); // 响应成功 + response.setSalt(salt); // 返回原始盐值 + response.setServerLease(LicenseServerUtils.leaseContent); // 服务器租约内容 + response.setServerUid(LicenseServerUtils.serverUid); // 服务器唯一标识 + response.setTicketId(RandomUtil.randomString(10)); // 生成随机凭证ID + + // 设置凭证属性,标识为个人许可证 + response.setTicketProperties(String.format( + "licensee=%s\tlicenseeType=5\tmetadata=0120211231PSAN000005", + hostName + )); // Personal License 个人许可证标识 + + response.setValidationDeadlinePeriod("-1"); // 无验证截止日期 + response.setValidationPeriod("600000"); // 验证周期(毫秒) + + log.debug("许可证凭证生成成功 - 凭证ID: {}", response.getTicketId()); + + // 返回签名后的XML数据 + return LicenseServerUtils.getSignXml(response); + } + + /** + * 许可证服务器连通性检查接口 + * + *

JetBrains产品会定期调用此接口来检查许可证服务器的可用性。 + * 该接口主要用于: + *

    + *
  • 验证服务器是否正常运行
  • + *
  • 检查网络连通性
  • + *
  • 更新服务器状态信息
  • + *
+ * + *

请求参数与 {@link #obtainTicket} 相同,但响应的内容略有不同。 + * + * @param request HTTP请求对象,包含客户端参数 + * @return 签名后的XML响应数据 + */ + @RequestMapping("/ping.action") + public Object ping(HttpServletRequest request) { + // 提取请求参数 + String hostName = request.getParameter("hostName"); + String machineId = request.getParameter("machineId"); + String salt = request.getParameter("salt"); + + log.debug("接收到Ping请求 - 主机: {}, 机器ID: {}", hostName, machineId); + + // 构建响应对象 + PingResponse response = new PingResponse(); + response.setAction("NONE"); // 无需额外操作 + response.setConfirmationStamp(LicenseServerUtils.getConfirmationStamp(machineId)); // 生成确认时间戳 + response.setLeaseSignature(LicenseServerUtils.getLeaseSignature()); // 生成租约签名 + response.setMessage(""); // 无错误消息 + response.setResponseCode("OK"); // 响应成功 + response.setSalt(salt); // 返回原始盐值 + response.setServerLease(LicenseServerUtils.leaseContent); // 服务器租约内容 + response.setServerUid(LicenseServerUtils.serverUid); // 服务器唯一标识 + response.setValidationDeadlinePeriod("-1"); // 无验证截止日期 + response.setValidationPeriod("600000"); // 验证周期(毫秒) + + log.debug("Ping响应生成成功"); + + // 返回签名后的XML数据 + return LicenseServerUtils.getSignXml(response); + } + + /** + * 延长许可证有效期接口 + * + *

当现有的许可证凭证即将过期时,JetBrains产品会调用此接口来延长凭证的有效期。 + * 该接口主要功能: + *

    + *
  • 更新许可证的有效期
  • + *
  • 延长凭证的使用时间
  • + *
  • 确保产品的连续使用
  • + *
+ * + *

此接口的响应格式与 {@link #obtainTicket} 基本相同。 + * + * @param request HTTP请求对象,包含客户端参数 + * @return 签名后的XML响应数据 + */ + @RequestMapping("/prolongTicket.action") + public Object prolongTicket(HttpServletRequest request) { + // 提取请求参数 + String hostName = request.getParameter("hostName"); + String machineId = request.getParameter("machineId"); + String salt = request.getParameter("salt"); + + log.debug("接收到延长许可证请求 - 主机: {}, 机器ID: {}", hostName, machineId); + + // 构建响应对象 + ProlongTicketResponse response = new ProlongTicketResponse(); + response.setAction("NONE"); // 无需额外操作 + response.setConfirmationStamp(LicenseServerUtils.getConfirmationStamp(machineId)); // 生成确认时间戳 + response.setLeaseSignature(LicenseServerUtils.getLeaseSignature()); // 生成租约签名 + response.setMessage(""); // 无错误消息 + response.setResponseCode("OK"); // 响应成功 + response.setSalt(salt); // 返回原始盐值 + response.setServerLease(LicenseServerUtils.leaseContent); // 服务器租约内容 + response.setServerUid(LicenseServerUtils.serverUid); // 服务器唯一标识 + response.setValidationDeadlinePeriod("-1"); // 无验证截止日期 + response.setValidationPeriod("600000"); // 验证周期(毫秒) + + log.debug("许可证延长响应生成成功"); + + // 返回签名后的XML数据 + return LicenseServerUtils.getSignXml(response); + } + + /** + * 释放许可证凭证接口 + * + *

当JetBrains产品关闭或用户主动退出时,会调用此接口来释放占用的许可证凭证。 + * 该接口主要功能: + *

    + *
  • 清理占用的许可证资源
  • + *
  • 释放服务器端的相关缓存
  • + *
  • 确保许可证能够被其他客户端使用
  • + *
+ * + *

此接口通常在产品关闭时自动调用,也可以手动调用来主动释放许可证。 + * + * @param request HTTP请求对象,包含客户端参数 + * @return 签名后的XML响应数据 + */ + @RequestMapping("/releaseTicket.action") + public Object releaseTicket(HttpServletRequest request) { + // 提取请求参数 + String hostName = request.getParameter("hostName"); + String machineId = request.getParameter("machineId"); + String salt = request.getParameter("salt"); + + log.debug("接收到释放许可证请求 - 主机: {}, 机器ID: {}", hostName, machineId); + + // 构建响应对象 + ReleaseTicketResponse response = new ReleaseTicketResponse(); + response.setAction("NONE"); // 无需额外操作 + response.setConfirmationStamp(LicenseServerUtils.getConfirmationStamp(machineId)); // 生成确认时间戳 + response.setLeaseSignature(LicenseServerUtils.getLeaseSignature()); // 生成租约签名 + response.setMessage(""); // 无错误消息 + response.setResponseCode("OK"); // 响应成功 + response.setSalt(salt); // 返回原始盐值 + response.setServerLease(LicenseServerUtils.leaseContent); // 服务器租约内容 + response.setServerUid(LicenseServerUtils.serverUid); // 服务器唯一标识 + response.setValidationDeadlinePeriod("-1"); // 无验证截止日期 + response.setValidationPeriod("600000"); // 验证周期(毫秒) + + log.debug("许可证释放响应生成成功"); + + // 返回签名后的XML数据 + return LicenseServerUtils.getSignXml(response); + } +} diff --git a/src/main/java/com/qiumo/help/controller/ZipController.java b/src/main/java/com/qiumo/help/controller/ZipController.java new file mode 100644 index 0000000..6a604fa --- /dev/null +++ b/src/main/java/com/qiumo/help/controller/ZipController.java @@ -0,0 +1,97 @@ +package com.qiumo.help.controller; + +import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; + +import cn.hutool.core.io.FileUtil; +import com.qiumo.help.context.AgentContextHolder; +import java.io.File; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 文件下载控制器 + * + *

此控制器专门用于提供文件下载服务,主要包括: + *

    + *
  • ja-netfilter代理工具的下载
  • + *
  • 其他相关工具文件的下载
  • + *
+ * + *

ja-netfilter是一个基于Java Instrumentation API实现的动态字节码修改工具, + * 主要用于在应用程序运行时动态修改字节码,实现特定的功能。 + * + *

下载的文件包含: + *

    + *
  • ja-netfilter核心程序
  • + *
  • 预配置的配置文件
  • + *
  • 相关的说明文档
  • + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +@Slf4j(topic = "文件下载") // 使用自定义日志主题 +@RestController +@RequiredArgsConstructor // Lombok自动生成构造函数 +public class ZipController { + + /** + * 下载ja-netfilter代理工具接口 + * + *

此接口提供预配置的ja-netfilter工具包下载服务。 + * ja-netfilter是一个强大的Java字节码修改工具,可以在运行时动态修改Java应用程序。 + * + *

下载的文件包括: + *

    + *
  • ja-netfilter.jar - 核心程序
  • + *
  • plugins/ - 各种功能插件
  • + *
  • config/ - 配置文件,已预配置了JetBrains产品的相关参数
  • + *
  • README.md - 使用说明
  • + *
+ * + *

使用方法: + *

    + *
  1. 下载并解压文件
  2. + *
  3. 在JetBrains IDE启动参数中添加:{@code -javaagent:/path/to/ja-netfilter.jar}
  4. + *
  5. 重新启动IDE即可
  6. + *
+ * + *

注意事项: + *

    + *
  • 请确保使用合法的许可证
  • + *
  • 仅供学习和研究使用
  • + *
  • 不得用于商业目的
  • + *
+ * + * @return 包含ja-netfilter工具的ZIP文件响应 + */ + @GetMapping("ja-netfilter") + public ResponseEntity downloadJaNetfilter() { + log.info("接收到ja-netfilter下载请求"); + + // 从AgentContextHolder获取ja-netfilter ZIP文件 + File jaNetfilterZipFile = AgentContextHolder.jaNetfilterZipFile(); + + log.debug("ja-netfilter文件路径: {}, 文件大小: {} 字节", + jaNetfilterZipFile.getAbsolutePath(), + jaNetfilterZipFile.length()); + + // 构建响应,设置为附件下载 + ResponseEntity response = ResponseEntity.ok() + .header(CONTENT_DISPOSITION, "attachment;filename=" + jaNetfilterZipFile.getName()) // 设置下载文件名 + .contentType(APPLICATION_OCTET_STREAM) // 设置内容类型为二进制流 + .body(new InputStreamResource(FileUtil.getInputStream(jaNetfilterZipFile))); // 设置响应体为文件流 + + log.info("ja-netfilter下载响应已生成"); + + return response; + } +} diff --git a/src/main/java/com/qiumo/help/model/ObtainTicketResponse.java b/src/main/java/com/qiumo/help/model/ObtainTicketResponse.java new file mode 100644 index 0000000..6ad990f --- /dev/null +++ b/src/main/java/com/qiumo/help/model/ObtainTicketResponse.java @@ -0,0 +1,88 @@ +package com.qiumo.help.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +/** + * 获取许可证凭证响应实体类 + * + *

此类封装了JetBrains许可证服务器在处理 "obtainTicket" 请求时返回的响应数据。 + * 该响应包含了许可证凭证的所有必要信息,包括凭证ID、属性、服务器信息等。 + * + *

使用JAXB注解支持XML序列化,保证与JetBrains官方许可证服务器协议的兼容性。 + * + *

响应结构说明: + *

    + *
  • action - 需要执行的操作,通常为 "NONE"
  • + *
  • confirmationStamp - 确认时间戳,用于验证请求的有效性
  • + *
  • leaseSignature - 租约签名,用于验证服务器的合法性
  • + *
  • ticketId - 凭证唯一标识符
  • + *
  • ticketProperties - 凭证属性,包含许可证类型和有效期信息
  • + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + + +@Data +@AllArgsConstructor +@NoArgsConstructor +@XmlRootElement(name = "ObtainTicketResponse") // XML根元素名称 +@XmlAccessorType(XmlAccessType.FIELD) // 使用字段直接访问方式 +public class ObtainTicketResponse implements Serializable { + + // ==================== 响应字段定义 ==================== + + /** 需要执行的动作,通常为 "NONE" 表示无需额外操作 */ + private String action; + + /** 确认时间戳,由服务器生成,用于验证请求的有效性和及时性 */ + private String confirmationStamp; + + /** 租约签名,用于验证服务器的合法性和权威性 */ + private String leaseSignature; + + /** 响应消息,通常为空,只在出现错误时包含错误信息 */ + private String message; + + /** 延长周期(毫秒),表示多久后可以申请延长许可证 */ + private String prolongationPeriod; + + /** 响应码,"OK" 表示成功,其他值表示各种错误情况 */ + private String responseCode; + + /** 加密盐值,从请求中原样返回,用于增强通信安全性 */ + private String salt; + + /** 服务器租约内容,包含过期时间和服务器标识 */ + private String serverLease; + + /** 服务器唯一标识符,用于标识许可证服务器实例 */ + private String serverUid; + + /** 许可证凭证的唯一标识符,由服务器随机生成 */ + private String ticketId; + + /** + * 许可证凭证属性,包含以下信息: + * - licensee: 许可证持有者名称 + * - licenseeType: 许可证类型(如 5 表示个人许可证) + * - metadata: 元数据信息,包含版本和有效期信息 + */ + private String ticketProperties; + + /** 验证截止时间(毫秒),-1 表示无截止时间 */ + private String validationDeadlinePeriod; + + /** 验证周期(毫秒),表示多久后需要重新验证 */ + private String validationPeriod; +} + diff --git a/src/main/java/com/qiumo/help/model/PingResponse.java b/src/main/java/com/qiumo/help/model/PingResponse.java new file mode 100644 index 0000000..0737dce --- /dev/null +++ b/src/main/java/com/qiumo/help/model/PingResponse.java @@ -0,0 +1,61 @@ +package com.qiumo.help.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +/** + * Ping请求响应实体类 + * + *

此类封装了JetBrains许可证服务器在处理 "ping" 请求时返回的响应数据。 + * Ping请求主要用于检查许可证服务器的可用性和连通性。 + * + *

与 {@link ObtainTicketResponse} 相比,此响应不包含许可证凭证相关信息, + * 主要用于确认服务器状态和更新连接信息。 + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@XmlRootElement(name = "PingResponse") // XML根元素名称 +@XmlAccessorType(XmlAccessType.FIELD) // 使用字段直接访问方式 +public class PingResponse implements Serializable { + + /** 需要执行的动作,通常为 "NONE" */ + private String action; + + /** 确认时间戳,用于验证请求的有效性 */ + private String confirmationStamp; + + /** 租约签名,用于验证服务器的合法性 */ + private String leaseSignature; + + /** 响应消息,通常为空 */ + private String message; + + /** 响应码,"OK" 表示成功 */ + private String responseCode; + + /** 加密盐值,从请求中原样返回 */ + private String salt; + + /** 服务器租约内容 */ + private String serverLease; + + /** 服务器唯一标识符 */ + private String serverUid; + + /** 验证截止时间,-1 表示无截止时间 */ + private String validationDeadlinePeriod; + + /** 验证周期,表示多久后需要重新验证 */ + private String validationPeriod; +} diff --git a/src/main/java/com/qiumo/help/model/ProlongTicketResponse.java b/src/main/java/com/qiumo/help/model/ProlongTicketResponse.java new file mode 100644 index 0000000..999adf9 --- /dev/null +++ b/src/main/java/com/qiumo/help/model/ProlongTicketResponse.java @@ -0,0 +1,61 @@ +package com.qiumo.help.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +/** + * 延长许可证凭证响应实体类 + * + *

此类封装了JetBrains许可证服务器在处理 "prolongTicket" 请求时返回的响应数据。 + * 延长请求用于在许可证凭证即将过期时延长其有效期。 + * + *

此响应的结构与 {@link ObtainTicketResponse} 基本相同, + * 但不包含新的凭证ID和属性,主要用于更新现有凭证的有效期。 + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@XmlRootElement(name = "ProlongTicketResponse") // XML根元素名称 +@XmlAccessorType(XmlAccessType.FIELD) // 使用字段直接访问方式 +public class ProlongTicketResponse implements Serializable { + + /** 需要执行的动作,通常为 "NONE" */ + private String action; + + /** 确认时间戳,用于验证请求的有效性 */ + private String confirmationStamp; + + /** 租约签名,用于验证服务器的合法性 */ + private String leaseSignature; + + /** 响应消息,通常为空 */ + private String message; + + /** 响应码,"OK" 表示成功 */ + private String responseCode; + + /** 加密盐值,从请求中原样返回 */ + private String salt; + + /** 服务器租约内容 */ + private String serverLease; + + /** 服务器唯一标识符 */ + private String serverUid; + + /** 验证截止时间,-1 表示无截止时间 */ + private String validationDeadlinePeriod; + + /** 验证周期,表示多久后需要重新验证 */ + private String validationPeriod; +} diff --git a/src/main/java/com/qiumo/help/model/ReleaseTicketResponse.java b/src/main/java/com/qiumo/help/model/ReleaseTicketResponse.java new file mode 100644 index 0000000..adb1f36 --- /dev/null +++ b/src/main/java/com/qiumo/help/model/ReleaseTicketResponse.java @@ -0,0 +1,61 @@ +package com.qiumo.help.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +/** + * 释放许可证凭证响应实体类 + * + *

此类封装了JetBrains许可证服务器在处理 "releaseTicket" 请求时返回的响应数据。 + * 释放请求用于在JetBrains产品关闭或用户主动退出时清理资源。 + * + *

此响应的结构与其他响应类型基本相同, + * 主要用于确认许可证凭证已成功释放和清理相关资源。 + * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@XmlRootElement(name = "ReleaseTicketResponse") // XML根元素名称 +@XmlAccessorType(XmlAccessType.FIELD) // 使用字段直接访问方式 +public class ReleaseTicketResponse implements Serializable { + + /** 需要执行的动作,通常为 "NONE" */ + private String action; + + /** 确认时间戳,用于验证请求的有效性 */ + private String confirmationStamp; + + /** 租约签名,用于验证服务器的合法性 */ + private String leaseSignature; + + /** 响应消息,通常为空 */ + private String message; + + /** 响应码,"OK" 表示成功 */ + private String responseCode; + + /** 加密盐值,从请求中原样返回 */ + private String salt; + + /** 服务器租约内容 */ + private String serverLease; + + /** 服务器唯一标识符 */ + private String serverUid; + + /** 验证截止时间,-1 表示无截止时间 */ + private String validationDeadlinePeriod; + + /** 验证周期,表示多久后需要重新验证 */ + private String validationPeriod; +} diff --git a/src/main/java/com/qiumo/help/service/JrebelService.java b/src/main/java/com/qiumo/help/service/JrebelService.java new file mode 100644 index 0000000..54d9968 --- /dev/null +++ b/src/main/java/com/qiumo/help/service/JrebelService.java @@ -0,0 +1,103 @@ +package com.qiumo.help.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.qiumo.help.util.jrebel.JrebelSign; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +@Service +public class JrebelService { + + private static final String SERVER_GUID = "a1b4aea8-b031-4302-b602-670a990272cb"; + + @SneakyThrows + public void jrebelLeasesHandler(HttpServletRequest request, HttpServletResponse response) { + response.setContentType("application/json; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + String clientRandomness = request.getParameter("randomness"); + String username = request.getParameter("username"); + String guid = request.getParameter("guid"); + boolean offline = true; + String validFrom = ""; + String validUntil = ""; + String clientTime = request.getParameter("clientTime"); + + try { + long clientTimeMillis = Long.parseLong(clientTime); + validFrom = clientTime; + validUntil = String.valueOf(clientTimeMillis + 180L * 24 * 60 * 60 * 1000); + } catch (NumberFormatException ignored) { + // 忽略非法输入,保持默认空值 + } + JSONObject jsonObject = JSONUtil.createObj().set("serverVersion", "3.2.4") + .set("serverProtocolVersion", "1.1") + .set("serverGuid", SERVER_GUID) + .set("groupType", "managed") + .set("id", 1) + .set("licenseType", 1) + .set("evaluationLicense", false) + .set("signature", + "OJE9wGg2xncSb+VgnYT+9HGCFaLOk28tneMFhCbpVMKoC/Iq4LuaDKPirBjG4o394/UjCDGgTBpIrzcXNPdVxVr8PnQzpy7ZSToGO8wv/KIWZT9/ba7bDbA8/RZ4B37YkCeXhjaixpmoyz/CIZMnei4q7oWR7DYUOlOcEWDQhiY=") + .set("serverRandomness", "H2ulzLlh7E0=") + .set("statusCode", "SUCCESS") + .set("offline", offline) + .set("validFrom", validFrom) + .set("validUntil", validUntil) + .set("company", "Administrator") + .set("orderId", "") + .set("zeroIds", new JSONArray()) + .set("licenseValidFrom", validFrom) + .set("licenseValidUntil", validUntil); + if (clientRandomness == null || username == null || guid == null) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } else { + JrebelSign jrebelSign = new JrebelSign(); + jrebelSign.toLeaseCreateJson(clientRandomness, guid, offline, validFrom, validUntil); + String signature = jrebelSign.getSignature(); + jsonObject.set("signature", signature); + jsonObject.set("company", username); + response.getWriter().write(jsonObject.toString()); + } + } + + @SneakyThrows + public void jrebelValidateHandler(HttpServletRequest request, HttpServletResponse response) { + response.setContentType("application/json; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + JSONObject jsonObject = JSONUtil.createObj().set("serverVersion", "3.2.4") + .set("serverProtocolVersion", "1.1") + .set("serverGuid", SERVER_GUID) + .set("groupType", "managed") + .set("statusCode", "SUCCESS") + .set("company", "Administrator") + .set("canGetLease", true) + .set("licenseType", 1) + .set("evaluationLicense", false) + .set("seatPoolType", "standalone"); + response.getWriter().write(jsonObject.toString()); + } + + @SneakyThrows + public void jrebelLeases1Handler(HttpServletRequest request, HttpServletResponse response) { + response.setContentType("application/json; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + JSONObject jsonObject = JSONUtil.createObj().set("serverVersion", "3.2.4") + .set("serverProtocolVersion", "1.1") + .set("serverGuid", SERVER_GUID) + .set("groupType", "managed") + .set("statusCode", "SUCCESS") + .set("msg", null) + .set("statusMessage", null); + String username = request.getParameter("username"); + if(ObjectUtil.isNotEmpty(username)){ + jsonObject.set("company", username); + } + response.getWriter().write(jsonObject.toString()); + } + +} diff --git a/src/main/java/com/qiumo/help/util/FileTools.java b/src/main/java/com/qiumo/help/util/FileTools.java new file mode 100644 index 0000000..10042ab --- /dev/null +++ b/src/main/java/com/qiumo/help/util/FileTools.java @@ -0,0 +1,144 @@ +package com.qiumo.help.util; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import org.springframework.boot.system.ApplicationHome; +import org.springframework.core.io.ClassPathResource; + +import java.io.*; + +/** + * 文件工具接口 + * + *

此接口提供了一系列静态方法用于处理应用程序中的文件操作。 + * 它能够智能地处理开发环境和生产环境下的文件路径问题, + * 并提供了从类路径复制文件到外部目录的功能。 + * + *

主要功能: + *

    + *
  • 文件存在性检查
  • + *
  • 智能文件路径解析(支持开发和生产环境)
  • + *
  • 自动从类路径复制文件到外部目录
  • + *
  • 目录结构的自动创建
  • + *
+ * + *

路径解析规则: + *

    + *
  • 开发环境:直接使用类路径中的文件
  • + *
  • 生产环境:JAR包启动时,使用应用目录下的文件
  • + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ + +public interface FileTools { + + // ==================== 常量定义 ==================== + + /** + * Spring Boot应用程序主目录工具 + * 用于获取应用程序的主目录和源文件信息 + */ + ApplicationHome application = new ApplicationHome(); + + // ==================== 核心方法 ==================== + + + /** + * 检查指定路径的文件是否存在 + * + *

此方法会根据当前的运行环境(开发或生产)来决定文件的实际位置。 + * 在开发环境中,直接从类路径中查找文件; + * 在生产环境中,从应用程序所在目录下查找文件。 + * + * @param path 相对路径,相对于类路径或应用程序目录 + * @return 如果文件存在则返回true,否则返回false + */ + static boolean fileExists(String path) { + return getFile(path).exists(); + } + + /** + * 获取指定路径的文件对象 + * + *

此方法是FileTools的核心方法,它会根据当前的运行环境智能地选择文件路径。 + * + *

路径解析规则: + *

    + *
  • 开发环境(source为null):直接使用类路径中的文件
  • + *
  • 生产环境(JAR包运行):使用应用程序所在目录下的文件
  • + *
+ * + *

这种设计允许应用程序在不同环境下都能正确地找到所需的文件。 + * + * @param path 相对路径,相对于类路径或应用程序目录 + * @return 文件对象(可能不存在) + */ + static File getFile(String path) { + // 获取应用程序的主目录和源文件 + File homeDir = application.getDir(); + File source = application.getSource(); + + // 创建类路径资源对象,用于在开发环境中访问文件 + ClassPathResource classPathResource = new ClassPathResource(path); + + // 根据运行环境返回不同的文件对象 + return ObjectUtil.isNull(source) + ? FileUtil.file(classPathResource.getPath()) // 开发环境:使用类路径 + : FileUtil.file(homeDir, path); // 生产环境:使用应用目录 + } + + /** + * 获取指定路径的文件对象,如果文件不存在则从类路径复制 + * + *

此方法是一个增强版的文件获取方法,它在 {@link #getFile(String)} 的基础上 + * 添加了自动复制功能。当目标文件不存在时,会尝试从类路径中复制文件。 + * + *

使用场景: + *

    + *
  • 首次运行时初始化配置文件
  • + *
  • 从内置资源复制模板文件
  • + *
  • 确保在生产环境中有必要的文件
  • + *
+ * + *

复制过程: + *

    + *
  1. 调用 {@link #getFile(String)} 获取目标文件对象
  2. + *
  3. 在生产环境中,检查类路径中的源文件是否存在
  4. + *
  5. 如果源文件存在但目标文件不存在,则进行复制操作
  6. + *
  7. 复制过程中会自动创建必要的目录结构
  8. + *
+ * + * @param path 相对路径,相对于类路径或应用程序目录 + * @return 文件对象(在此方法返回后应该存在) + * @throws IllegalArgumentException 当文件读取或复制失败时抛出 + */ + static File getFileOrCreat(String path) { + // 获取目标文件对象 + File file = getFile(path); + + // 在生产环境中处理文件复制 + if (ObjectUtil.isNotNull(application.getSource())) { + // 创建类路径资源对象 + ClassPathResource classPathResource = new ClassPathResource(path); + + // 检查是否需要从类路径复制文件 + if (classPathResource.exists() && !file.exists()) { + try (InputStream inputStream = classPathResource.getInputStream()) { + // 使用Hutool的文件工具进行复制 + FileUtil.writeFromStream(inputStream, file); + } catch (Exception e) { + // 抛出友好的错误信息 + throw new IllegalArgumentException( + CharSequenceUtil.format("{} 文件读取失败!", path), e + ); + } + } + } + + return file; + } +} diff --git a/src/main/java/com/qiumo/help/util/LicenseServerUtils.java b/src/main/java/com/qiumo/help/util/LicenseServerUtils.java new file mode 100644 index 0000000..cadf80e --- /dev/null +++ b/src/main/java/com/qiumo/help/util/LicenseServerUtils.java @@ -0,0 +1,250 @@ +package com.qiumo.help.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IoUtil; +import cn.hutool.crypto.PemUtil; +import com.qiumo.help.context.CertificateContextHolder; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.Signature; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * 许可证服务器工具类 + * + *

此工具类为许可证服务器提供各种工具方法,主要包括: + *

    + *
  • RSA数字签名功能
  • + *
  • 确认时间戳生成
  • + *
  • 租约签名生成
  • + *
  • XML数据签名和序列化
  • + *
+ * + *

此类的所有方法都是静态的,不需要实例化。 + * 所有的密码学操作都基于CertificateContextHolder中管理的证书和密钥。 + * + *

安全性说明: + *

    + *
  • 使用RSA非对称加密算法
  • + *
  • 支持SHA1withRSA和SHA512withRSA签名算法
  • + *
  • 所有签名都包含相应的X.509证书信息
  • + *
+ * + * @author QiuMo + * @version 1.0.0 + * @since 1.0.0 + */ +@Slf4j(topic = "许可证服务器工具") +@NoArgsConstructor(access = AccessLevel.PRIVATE) // 私有构造函数,防止实例化 + +public class LicenseServerUtils { + + // ==================== 常量定义 ==================== + + /** 服务器唯一标识符,用于标识许可证服务器实例 */ + public static final String serverUid = "qiumo"; + + /** + * 服务器租约内容,格式为:过期时间戳:服务器UID + * 4102415999000 对应的日期为 2099-12-31 23:59:59,表示许可证长期有效 + */ + public static final String leaseContent = "4102415999000:" + serverUid; + + // ==================== 核心方法 ==================== + + /** + * 使用指定的私钥和算法对内容进行数字签名 + * + *

此方法是许可证服务器的核心签名功能,用于确保数据的完整性和可靠性。 + * 签名过程采用标准的RSA数字签名算法,结果使用Base64编码。 + * + *

支持的签名算法: + *

    + *
  • SHA1withRSA - 适用于一般安全级别
  • + *
  • SHA256withRSA - 适用于高安全级别
  • + *
  • SHA512withRSA - 适用于最高安全级别
  • + *
+ * + * @param content 需要签名的内容字符串 + * @param privateKey RSA私钥,用于生成数字签名 + * @param signAlgorithm 签名算法,如 "SHA1withRSA"、"SHA256withRSA" 等 + * @return Base64编码的数字签名字符串 + * @throws RuntimeException 当签名过程发生错误时抛出 + */ + @SneakyThrows + public static String signContent(String content, PrivateKey privateKey, String signAlgorithm) { + log.debug("开始数字签名 - 算法: {}, 内容长度: {} 字符", signAlgorithm, content.length()); + + // 初始化签名器 + Signature signature = Signature.getInstance(signAlgorithm); + signature.initSign(privateKey); + + // 对内容进行签名(使用UTF-8编码) + signature.update(content.getBytes(StandardCharsets.UTF_8)); + + // 生成签名并转换为Base64格式 + String signedContent = Base64.encode(signature.sign()); + + log.debug("数字签名完成 - 签名长度: {} 字符", signedContent.length()); + + return signedContent; + } + + /** + * 生成机器确认时间戳 + * + *

确认时间戳用于验证客户端请求的有效性和及时性。 + * 它结合了当前时间戳、机器ID和数字签名,确保请求的安全性。 + * + *

确认时间戳格式: + * {@code 时间戳:机器ID:签名算法:签名内容:证书Base64} + * + *

生成过程: + *

    + *
  1. 获取当前系统时间戳
  2. + *
  3. 构建签名内容:时间戳 + ":" + 机器ID
  4. + *
  5. 使用SHA1withRSA算法进行数字签名
  6. + *
  7. 获取服务器子证书并转换为Base64
  8. + *
  9. 组装最终的确认时间戳字符串
  10. + *
+ * + * @param machineId 客户端机器的唯一标识符 + * @return 组装好的确认时间戳字符串 + * @throws RuntimeException 当签名或证书处理发生错误时抛出 + */ + public static String getConfirmationStamp(String machineId) { + log.debug("开始生成确认时间戳 - 机器ID: {}", machineId); + + // 获取当前时间戳 + long timeStamp = System.currentTimeMillis(); + + // 构建需要签名的内容:时间戳 + ":" + 机器ID + String contentToSign = timeStamp + ":" + machineId; + + // 获取私钥并进行数字签名 + PrivateKey privateKey = PemUtil.readPemPrivateKey(IoUtil.toStream(CertificateContextHolder.privateKeyFile())); + String signature = signContent(contentToSign, privateKey, "SHA1withRSA"); + + // 获取服务器子证书并转换为Base64 + String certificateBase64 = Base64.encode(PemUtil.readPem(IoUtil.toStream(CertificateContextHolder.serverChildCrtFile()))); + + // 组装最终的确认时间戳 + String confirmationStamp = timeStamp + ":" + machineId + ":" + "SHA1withRSA" + ":" + signature + ":" + certificateBase64; + + log.debug("确认时间戳生成完成 - 时间戳: {}", timeStamp); + + return confirmationStamp; + } + + /** + * 生成服务器租约签名 + * + *

租约签名用于验证服务器的合法性和权威性。 + * 它对预定义的租约内容进行数字签名,并附带相应的证书信息。 + * + *

租约签名格式: + * {@code 签名算法-签名内容-证书Base64} + * + *

生成过程: + *

    + *
  1. 使用SHA512withRSA算法对租约内容进行数字签名
  2. + *
  3. 获取授权码证书并转换为Base64
  4. + *
  5. 按照指定格式组装签名结果
  6. + *
+ * + *

使用SHA512withRSA算法提供更高的安全性,适合于重要的服务器认证场景。 + * + * @return 组装好的租约签名字符串 + * @throws RuntimeException 当签名或证书处理发生错误时抛出 + */ + public static String getLeaseSignature() { + log.debug("开始生成租约签名 - 租约内容: {}", leaseContent); + + // 获取私钥并使用SHA512withRSA算法进行签名 + PrivateKey privateKey = PemUtil.readPemPrivateKey(IoUtil.toStream(CertificateContextHolder.privateKeyFile())); + String signature = signContent(leaseContent, privateKey, "SHA512withRSA"); + + // 获取授权码证书并转换为Base64 + String certificateBase64 = Base64.encode(PemUtil.readPem(IoUtil.toStream(CertificateContextHolder.codeCrtFile()))); + + // 按照格式组装租约签名 + String leaseSignature = "SHA512withRSA-" + signature + "-" + certificateBase64; + + log.debug("租约签名生成完成"); + + return leaseSignature; + } + + /** + * 对XML数据进行数字签名并生成完整的XML响应 + * + *

此方法是许可证服务器响应生成的核心方法。 + * 它将Java对象序列化为XML,然后对XML内容进行数字签名, + * 最后在XML头部添加签名注释。 + * + *

XML签名格式: + *

+     * <!-- SHA1withRSA-签名内容-证书Base64 -->
+     * <ResponseObject>
+     *   ...实际XML内容...
+     * </ResponseObject>
+     * 
+ * + *

序列化过程: + *

    + *
  1. 使用JAXB将Java对象转换为XML字符串
  2. + *
  3. 移除XML头部声明(设置JAXB_FRAGMENT为true)
  4. + *
  5. 对XML内容使用SHA1withRSA进行数字签名
  6. + *
  7. 获取服务器子证书并转换为Base64
  8. + *
  9. 在XML内容前添加包含签名信息的注释
  10. + *
+ * + *

此方法保证了XML响应的完整性和可验证性, + * JetBrains客户端可以通过验证签名来确认响应的真实性。 + * + * @param xmlObject 需要签名的Java对象,必须具有JAXB注解 + * @return 带有数字签名的完整XML字符串 + * @throws RuntimeException 当XML序列化或签名过程发生错误时抛出 + */ + @SneakyThrows + public static String getSignXml(Object xmlObject) { + log.debug("开始生成签名XML - 对象类型: {}", xmlObject.getClass().getSimpleName()); + + // 初始化JAXB上下文 + JAXBContext jaxbContext = JAXBContext.newInstance(xmlObject.getClass()); + + // 创建序列化器 + Marshaller marshaller = jaxbContext.createMarshaller(); + + // 设置序列化参数:去除XML头部声明 + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); + + // 将Java对象序列化为XML字符串 + StringWriter writer = new StringWriter(); + marshaller.marshal(xmlObject, writer); + String xmlContent = writer.toString(); + + log.debug("XML序列化完成 - XML长度: {} 字符", xmlContent.length()); + + // 获取私钥并对XML内容进行数字签名 + PrivateKey privateKey = PemUtil.readPemPrivateKey(IoUtil.toStream(CertificateContextHolder.privateKeyFile())); + String signature = signContent(xmlContent, privateKey, "SHA1withRSA"); + + // 获取服务器子证书并转换为Base64 + String certificateBase64 = Base64.encode(PemUtil.readPem(IoUtil.toStream(CertificateContextHolder.serverChildCrtFile()))); + + // 构建带有签名注释的完整XML + String signedXml = "\n" + xmlContent; + + log.debug("签名XML生成完成 - 最终长度: {} 字符", signedXml.length()); + + return signedXml; + } + +} diff --git a/src/main/java/com/qiumo/help/util/jrebel/ByteUtil.java b/src/main/java/com/qiumo/help/util/jrebel/ByteUtil.java new file mode 100644 index 0000000..3d0c9ad --- /dev/null +++ b/src/main/java/com/qiumo/help/util/jrebel/ByteUtil.java @@ -0,0 +1,34 @@ +package com.qiumo.help.util.jrebel; + +import java.nio.charset.Charset; +import java.util.Base64; +import java.util.Random; + +public class ByteUtil { + + private static final Random a; + + public static String a(final byte[] binaryData) { + if (binaryData == null) { + return null; + } + return new String(Base64.getEncoder().encode(binaryData), Charset.forName("UTF-8")); + } + + public static byte[] a(final String s) { + if (s == null) { + return null; + } + return Base64.getDecoder().decode(s.getBytes(Charset.forName("UTF-8"))); + } + + public static byte[] a(final int n) { + final byte[] array = new byte[n]; + ByteUtil.a.nextBytes(array); + return array; + } + + static { + a = new Random(); + } +} diff --git a/src/main/java/com/qiumo/help/util/jrebel/JrebelSign.java b/src/main/java/com/qiumo/help/util/jrebel/JrebelSign.java new file mode 100644 index 0000000..d7e6371 --- /dev/null +++ b/src/main/java/com/qiumo/help/util/jrebel/JrebelSign.java @@ -0,0 +1,30 @@ +package com.qiumo.help.util.jrebel; + +import cn.hutool.core.collection.CollUtil; + +public class JrebelSign { + + private String signature; + + public void toLeaseCreateJson(String clientRandomness, String guid, boolean offline, String validFrom, + String validUntil) { + String serverRandomness = "H2ulzLlh7E0="; //服务端随机数,如果要自己生成,务必将其写到json的serverRandomness中 + String installationGuidString = guid; + String s2 = ""; + if (offline) { + s2 = CollUtil.join( + CollUtil.newArrayList(clientRandomness, serverRandomness, installationGuidString, + String.valueOf(offline), validFrom, validUntil), ";"); + } else { + s2 = CollUtil.join(CollUtil.newArrayList(clientRandomness, serverRandomness, installationGuidString, + String.valueOf(offline)), ";"); + } + final byte[] a2 = LicenseServer2ToJRebelPrivateKey.a(s2.getBytes()); + this.signature = ByteUtil.a(a2); + } + + public String getSignature() { + return signature; + } + +} diff --git a/src/main/java/com/qiumo/help/util/jrebel/LicenseServer2ToJRebelPrivateKey.java b/src/main/java/com/qiumo/help/util/jrebel/LicenseServer2ToJRebelPrivateKey.java new file mode 100644 index 0000000..347f122 --- /dev/null +++ b/src/main/java/com/qiumo/help/util/jrebel/LicenseServer2ToJRebelPrivateKey.java @@ -0,0 +1,44 @@ +package com.qiumo.help.util.jrebel; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; + +public class LicenseServer2ToJRebelPrivateKey { + + private static final String b = "MIICXAIBAAKBgQDQ93CP6SjEneDizCF1P/MaBGf582voNNFcu8oMhgdTZ/N6qa6O7XJDr1FSCyaDdKSsPCdxPK7Y4Usq/fOPas2kCgYcRS/iebrtPEFZ/7TLfk39HLuTEjzo0/CNvjVsgWeh9BYznFaxFDLx7fLKqCQ6w1OKScnsdqwjpaXwXqiulwIDAQABAoGATOQvvBSMVsTNQkbgrNcqKdGjPNrwQtJkk13aO/95ZJxkgCc9vwPqPrOdFbZappZeHa5IyScOI2nLEfe+DnC7V80K2dBtaIQjOeZQt5HoTRG4EHQaWoDh27BWuJoip5WMrOd+1qfkOtZoRjNcHl86LIAh/+3vxYyebkug4UHNGPkCQQD+N4ZUkhKNQW7mpxX6eecitmOdN7Yt0YH9UmxPiW1LyCEbLwduMR2tfyGfrbZALiGzlKJize38shGC1qYSMvZFAkEA0m6psWWiTUWtaOKMxkTkcUdigalZ9xFSEl6jXFB94AD+dlPS3J5gNzTEmbPLc14VIWJFkO+UOrpl77w5uF2dKwJAaMpslhnsicvKMkv31FtBut5iK6GWeEafhdPfD94/bnidpP362yJl8Gmya4cI1GXvwH3pfj8S9hJVA5EFvgTB3QJBAJP1O1uAGp46X7Nfl5vQ1M7RYnHIoXkWtJ417Kb78YWPLVwFlD2LHhuy/okT4fk8LZ9LeZ5u1cp1RTdLIUqAiAECQC46OwOm87L35yaVfpUIjqg/1gsNwNsj8HvtXdF/9d30JIM3GwdytCvNRLqP35Ciogb9AO8ke8L6zY83nxPbClM="; + private static final byte[] c; + private static final BouncyCastleProvider d; + + private static PrivateKey a() { + final PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(LicenseServer2ToJRebelPrivateKey.c); + try { + return KeyFactory.getInstance("RSA", LicenseServer2ToJRebelPrivateKey.d) + .generatePrivate(pkcs8EncodedKeySpec); + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + public static byte[] a(final byte[] array) { + try { + final Signature instance = Signature.getInstance("SHA1withRSA", LicenseServer2ToJRebelPrivateKey.d); + instance.initSign(a()); + instance.update(array); + return instance.sign(); + } catch (GeneralSecurityException ex) { + throw new RuntimeException("License Server installation error 0000000F2", ex); + } + } + + static { + c = ByteUtil.a( + "MIICXAIBAAKBgQDQ93CP6SjEneDizCF1P/MaBGf582voNNFcu8oMhgdTZ/N6qa6O7XJDr1FSCyaDdKSsPCdxPK7Y4Usq/fOPas2kCgYcRS/iebrtPEFZ/7TLfk39HLuTEjzo0/CNvjVsgWeh9BYznFaxFDLx7fLKqCQ6w1OKScnsdqwjpaXwXqiulwIDAQABAoGATOQvvBSMVsTNQkbgrNcqKdGjPNrwQtJkk13aO/95ZJxkgCc9vwPqPrOdFbZappZeHa5IyScOI2nLEfe+DnC7V80K2dBtaIQjOeZQt5HoTRG4EHQaWoDh27BWuJoip5WMrOd+1qfkOtZoRjNcHl86LIAh/+3vxYyebkug4UHNGPkCQQD+N4ZUkhKNQW7mpxX6eecitmOdN7Yt0YH9UmxPiW1LyCEbLwduMR2tfyGfrbZALiGzlKJize38shGC1qYSMvZFAkEA0m6psWWiTUWtaOKMxkTkcUdigalZ9xFSEl6jXFB94AD+dlPS3J5gNzTEmbPLc14VIWJFkO+UOrpl77w5uF2dKwJAaMpslhnsicvKMkv31FtBut5iK6GWeEafhdPfD94/bnidpP362yJl8Gmya4cI1GXvwH3pfj8S9hJVA5EFvgTB3QJBAJP1O1uAGp46X7Nfl5vQ1M7RYnHIoXkWtJ417Kb78YWPLVwFlD2LHhuy/okT4fk8LZ9LeZ5u1cp1RTdLIUqAiAECQC46OwOm87L35yaVfpUIjqg/1gsNwNsj8HvtXdF/9d30JIM3GwdytCvNRLqP35Ciogb9AO8ke8L6zY83nxPbClM="); + d = new BouncyCastleProvider(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..53febc9 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + application: + name: QiuMo-Jetbrains-Help +server: + port: 10768 +help: + default-license-name: QiuMo + default-assignee-name: 囚墨 + default-expiry-date: 2111-11-11 diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..243b2c4 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +${AnsiColor.RED} ██╗███████╗████████╗██████╗ ██████╗ █████╗ ██╗███╗ ██╗███████╗ ██╗ ██╗███████╗██╗ ██████╗ +${AnsiColor.CYAN} ██║██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██║████╗ ██║██╔════╝ ██║ ██║██╔════╝██║ ██╔══██╗ +${AnsiColor.BRIGHT_YELLOW} ██║█████╗ ██║ ██████╔╝██████╔╝███████║██║██╔██╗ ██║███████╗█████╗███████║█████╗ ██║ ██████╔╝ +${AnsiColor.GREEN}██ ██║██╔══╝ ██║ ██╔══██╗██╔══██╗██╔══██║██║██║╚██╗██║╚════██║╚════╝██╔══██║██╔══╝ ██║ ██╔═══╝ +${AnsiColor.BLUE}╚█████╔╝███████╗ ██║ ██████╔╝██║ ██║██║ ██║██║██║ ╚████║███████║ ██║ ██║███████╗███████╗██║ +${AnsiColor.MAGENTA} ╚════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ + +${AnsiColor.BRIGHT_YELLOW} Spring Boot Version: ${spring-boot.version}${AnsiColor.DEFAULT} \ No newline at end of file diff --git a/src/main/resources/external/agent/ja-netfilter.zip b/src/main/resources/external/agent/ja-netfilter.zip new file mode 100644 index 0000000000000000000000000000000000000000..6f715c633ef5b37755e6c284a81f3a23f852d689 GIT binary patch literal 85423 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

V;iib3n<%v0qS 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/external/certificate/code-root.key b/src/main/resources/external/certificate/code-root.key new file mode 100644 index 0000000..fafaa78 --- /dev/null +++ b/src/main/resources/external/certificate/code-root.key @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFOzCCAyOgAwIBAgIJANJssYOyg3nhMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDUpldFByb2ZpbGUgQ0EwHhcNMTUxMDAyMTEwMDU2WhcNNDUxMDI0MTEwMDU2 +WjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMIICIjANBgkqhkiG9w0BAQEFAAOC +Ag8AMIICCgKCAgEA0tQuEA8784NabB1+T2XBhpB+2P1qjewHiSajAV8dfIeWJOYG +y+ShXiuedj8rL8VCdU+yH7Ux/6IvTcT3nwM/E/3rjJIgLnbZNerFm15Eez+XpWBl +m5fDBJhEGhPc89Y31GpTzW0vCLmhJ44XwvYPntWxYISUrqeR3zoUQrCEp1C6mXNX +EpqIGIVbJ6JVa/YI+pwbfuP51o0ZtF2rzvgfPzKtkpYQ7m7KgA8g8ktRXyNrz8bo +iwg7RRPeqs4uL/RK8d2KLpgLqcAB9WDpcEQzPWegbDrFO1F3z4UVNH6hrMfOLGVA +xoiQhNFhZj6RumBXlPS0rmCOCkUkWrDr3l6Z3spUVgoeea+QdX682j6t7JnakaOw +jzwY777SrZoi9mFFpLVhfb4haq4IWyKSHR3/0BlWXgcgI6w6LXm+V+ZgLVDON52F +LcxnfftaBJz2yclEwBohq38rYEpb+28+JBvHJYqcZRaldHYLjjmb8XXvf2MyFeXr +SopYkdzCvzmiEJAewrEbPUaTllogUQmnv7Rv9sZ9jfdJ/cEn8e7GSGjHIbnjV2ZM +Q9vTpWjvsT/cqatbxzdBo/iEg5i9yohOC9aBfpIHPXFw+fEj7VLvktxZY6qThYXR +Rus1WErPgxDzVpNp+4gXovAYOxsZak5oTV74ynv1aQ93HSndGkKUE/qA/JECAwEA +AaOBhzCBhDAdBgNVHQ4EFgQUo562SGdCEjZBvW3gubSgUouX8bMwSAYDVR0jBEEw +P4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2Zp +bGUgQ0GCCQDSbLGDsoN54TAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq +hkiG9w0BAQsFAAOCAgEAjrPAZ4xC7sNiSSqh69s3KJD3Ti4etaxcrSnD7r9rJYpK +BMviCKZRKFbLv+iaF5JK5QWuWdlgA37ol7mLeoF7aIA9b60Ag2OpgRICRG79QY7o +uLviF/yRMqm6yno7NYkGLd61e5Huu+BfT459MWG9RVkG/DY0sGfkyTHJS5xrjBV6 +hjLG0lf3orwqOlqSNRmhvn9sMzwAP3ILLM5VJC5jNF1zAk0jrqKz64vuA8PLJZlL +S9TZJIYwdesCGfnN2AETvzf3qxLcGTF038zKOHUMnjZuFW1ba/12fDK5GJ4i5y+n +fDWVZVUDYOPUixEZ1cwzmf9Tx3hR8tRjMWQmHixcNC8XEkVfztID5XeHtDeQ+uPk +X+jTDXbRb+77BP6n41briXhm57AwUI3TqqJFvoiFyx5JvVWG3ZqlVaeU/U9e0gxn +8qyR+ZA3BGbtUSDDs8LDnE67URzK+L+q0F2BC758lSPNB2qsJeQ63bYyzf0du3wB +/gb2+xJijAvscU3KgNpkxfGklvJD/oDUIqZQAnNcHe7QEf8iG2WqaMJIyXZlW3me +0rn+cgvxHPt6N4EBh5GgNZR4l0eaFEV+fxVsydOQYo1RIyFMXtafFBqQl6DDxujl +FeU3FZ+Bcp12t7dlM4E0/sS1XdL47CfGVj4Bp+/VbF862HmkAbd7shs7sDQkHbU= +-----END CERTIFICATE----- diff --git a/src/main/resources/external/certificate/server-root.key b/src/main/resources/external/certificate/server-root.key new file mode 100644 index 0000000..e89c72c --- /dev/null +++ b/src/main/resources/external/certificate/server-root.key @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFTDCCAzSgAwIBAgIJAMCrW9HV+hjZMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV +BAMMEkxpY2Vuc2UgU2VydmVycyBDQTAgFw0xNjEwMTIxNDMwNTRaGA8yMTE2MTIy +NzE0MzA1NFowHTEbMBkGA1UEAwwSTGljZW5zZSBTZXJ2ZXJzIENBMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoT7LvHj3JKK2pgc5f02z+xEiJDcvlBi6 +fIwrg/504UaMx3xWXAE5CEPelFty+QPRJnTNnSxqKQQmg2s/5tMJpL9lzGwXaV7a +rrcsEDbzV4el5mIXUnk77Bm/QVv48s63iQqUjVmvjQt9SWG2J7+h6X3ICRvF1sQB +yeat/cO7tkpz1aXXbvbAws7/3dXLTgAZTAmBXWNEZHVUTcwSg2IziYxL8HRFOH0+ +GMBhHqa0ySmF1UTnTV4atIXrvjpABsoUvGxw+qOO2qnwe6ENEFWFz1a7pryVOHXg +P+4JyPkI1hdAhAqT2kOKbTHvlXDMUaxAPlriOVw+vaIjIVlNHpBGhqTj1aqfJpLj +qfDFcuqQSI4O1W5tVPRNFrjr74nDwLDZnOF+oSy4E1/WhL85FfP3IeQAIHdswNMJ +y+RdkPZCfXzSUhBKRtiM+yjpIn5RBY+8z+9yeGocoxPf7l0or3YF4GUpud202zgy +Y3sJqEsZksB750M0hx+vMMC9GD5nkzm9BykJS25hZOSsRNhX9InPWYYIi6mFm8QA +2Dnv8wxAwt2tDNgqa0v/N8OxHglPcK/VO9kXrUBtwCIfZigO//N3hqzfRNbTv/ZO +k9lArqGtcu1hSa78U4fuu7lIHi+u5rgXbB6HMVT3g5GQ1L9xxT1xad76k2EGEi3F +9B+tSrvru70CAwEAAaOBjDCBiTAdBgNVHQ4EFgQUpsRiEz+uvh6TsQqurtwXMd4J +8VEwTQYDVR0jBEYwRIAUpsRiEz+uvh6TsQqurtwXMd4J8VGhIaQfMB0xGzAZBgNV +BAMMEkxpY2Vuc2UgU2VydmVycyBDQYIJAMCrW9HV+hjZMAwGA1UdEwQFMAMBAf8w +CwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCJ9+GQWvBS3zsgPB+1PCVc +oG6FY87N6nb3ZgNTHrUMNYdo7FDeol2DSB4wh/6rsP9Z4FqVlpGkckB+QHCvqU+d +rYPe6QWHIb1kE8ftTnwapj/ZaBtF80NWUfYBER/9c6To5moW63O7q6cmKgaGk6zv +St2IhwNdTX0Q5cib9ytE4XROeVwPUn6RdU/+AVqSOspSMc1WQxkPVGRF7HPCoGhd +vqebbYhpahiMWfClEuv1I37gJaRtsoNpx3f/jleoC/vDvXjAznfO497YTf/GgSM2 +LCnVtpPQQ2vQbOfTjaBYO2MpibQlYpbkbjkd5ZcO5U5PGrQpPFrWcylz7eUC3c05 +UVeygGIthsA/0hMCioYz4UjWTgi9NQLbhVkfmVQ5lCVxTotyBzoubh3FBz+wq2Qt +iElsBrCMR7UwmIu79UYzmLGt3/gBdHxaImrT9SQ8uqzP5eit54LlGbvGekVdAL5l +DFwPcSB1IKauXZvi1DwFGPeemcSAndy+Uoqw5XGRqE6jBxS7XVI7/4BSMDDRBz1u +a+JMGZXS8yyYT+7HdsybfsZLvkVmc9zVSDI7/MjVPdk6h0sLn+vuPC1bIi5edoNy +PdiG2uPH5eDO6INcisyPpLS4yFKliaO4Jjap7yzLU9pbItoWgCAYa2NpxuxHJ0tB +7tlDFnvaRnQukqSG+VqNWg== +-----END CERTIFICATE----- diff --git a/src/main/resources/external/data/plugin.json b/src/main/resources/external/data/plugin.json new file mode 100644 index 0000000..8352ff8 --- /dev/null +++ b/src/main/resources/external/data/plugin.json @@ -0,0 +1,2597 @@ +[ + { + "id": 25828, + "productCode": "PLOG", + "name": ".log", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25828/694715/icon/default.svg" + }, + { + "id": 26365, + "productCode": "PACTIONATE", + "name": "Actionate", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26365/689903/icon/default.svg" + }, + { + "id": 9269, + "productCode": "PAEMIDE", + "name": "AEM IDE", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/9269/658192/icon/default.svg" + }, + { + "id": 9863, + "productCode": "PAEM", + "name": "AEM Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/9863/633318/icon/default.svg" + }, + { + "id": 21263, + "productCode": "PAICODING", + "name": "AI Coding", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21263/693649/icon/default.svg" + }, + { + "id": 21289, + "productCode": "PCAICOMMITAPP", + "name": "AICommit", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21289/658026/icon/default.svg" + }, + { + "id": 24318, + "productCode": "PDLXASAS", + "name": "Alibaba Sentinel Annotation Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24318/598743/icon/default.svg" + }, + { + "id": 24767, + "productCode": "PDLXASASF", + "name": "Alibaba Sentinel Annotation Support Freemium", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24767/598742/icon/default.svg" + }, + { + "id": 12408, + "productCode": "PBISAA", + "name": "Android Antidecompiler", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12408/229970/icon/default.svg" + }, + { + "id": 15456, + "productCode": "PAPH", + "name": "Android Package Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15456/104869/icon/default.svg" + }, + { + "id": 13156, + "productCode": "PWIFIADB", + "name": "Android WiFiADB", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13156/154275/icon/default.svg" + }, + { + "id": 26478, + "productCode": "PANNOTE", + "name": "AnNote", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26478/698814/icon/default.svg" + }, + { + "id": 9707, + "productCode": "PANSIHIGHLIGHT", + "name": "ansi-highlighter-premium", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/9707/698973/icon/default.svg" + }, + { + "id": 18357, + "productCode": "PCDAPIRUNNER", + "name": "API Runner", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18357/269544/icon/default.svg" + }, + { + "id": 16682, + "productCode": "PAPPLETRUNNER", + "name": "Applet Runner", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/16682/686767/icon/default.svg" + }, + { + "id": 21566, + "productCode": "PARMADILLO", + "name": "Armadillo", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/21566/354792/icon/default.svg" + }, + { + "id": 26817, + "productCode": "PARTHASENHANCEM", + "name": "Arthas Enhance Mybatis", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26817/699489/icon/default.svg" + }, + { + "id": 17699, + "productCode": "PASTOCK", + "name": "AStock", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17699/674987/icon/default.svg" + }, + { + "id": 13016, + "productCode": "PATOMONEDARK", + "name": "Atom One Dark By Mayke", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13016/122178/icon/default.svg" + }, + { + "id": 25063, + "productCode": "PAUTOAPIGENERAT", + "name": "Auto API Generator", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25063/680255/icon/default.svg" + }, + { + "id": 14088, + "productCode": "PGOLANGCODESUGG", + "name": "Auto GOLang Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14088/191484/icon/default.svg" + }, + { + "id": 14070, + "productCode": "PJAVACODESUGG", + "name": "Auto Java Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14070/191711/icon/default.svg" + }, + { + "id": 14166, + "productCode": "PJSCODESUGG", + "name": "Auto Javascript Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14166/191485/icon/default.svg" + }, + { + "id": 14167, + "productCode": "PPHPCODESUGG", + "name": "Auto PHP Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14167/191710/icon/default.svg" + }, + { + "id": 14594, + "productCode": "PPYCODESUGG", + "name": "Auto Python Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14594/190896/icon/default.svg" + }, + { + "id": 14089, + "productCode": "PRUBYCODESUGG", + "name": "Auto Ruby Code Suggestions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14089/191482/icon/default.svg" + }, + { + "id": 10904, + "productCode": "PBRWJV", + "name": "AutoCode for Java", + "pricingModel": "PAID" + }, + { + "id": 23577, + "productCode": "PAUTOLOG", + "name": "AutoLog", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23577/473858/icon/default.svg" + }, + { + "id": 26223, + "productCode": "PCODEPIPELINE", + "name": "AWS CodePipeline Dashboard - Execute & Monitor Pipelines", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26223/658810/icon/default.svg" + }, + { + "id": 14742, + "productCode": "PAWSLAMBDADEPLR", + "name": "AWS Lambda Deployer", + "pricingModel": "PAID" + }, + { + "id": 22319, + "productCode": "PAZD", + "name": "Azd", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22319/694698/icon/default.svg" + }, + { + "id": 22194, + "productCode": "PAZURECODING", + "name": "Azure Coding", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22194/355932/icon/default.svg" + }, + { + "id": 25737, + "productCode": "PBACKLOG", + "name": "Backlog Integration", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25737/635537/icon/default.svg" + }, + { + "id": 25896, + "productCode": "PBASEFILE", + "name": "Base64 File", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25896/690603/icon/default.svg" + }, + { + "id": 13841, + "productCode": "PBASHSUPPORTPRO", + "name": "BashSupport Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13841/698085/icon/default.svg" + }, + { + "id": 12895, + "productCode": "PBETTERHIGHLIGH", + "name": "Better Highlights", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/12895/681976/icon/default.svg" + }, + { + "id": 26356, + "productCode": "PBETTERXTOOLS", + "name": "BetterX Tools", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26356/683596/icon/default.svg" + }, + { + "id": 13538, + "productCode": "PCREVIEW", + "name": "Bitbucket Integration Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13538/680759/icon/default.svg" + }, + { + "id": 16222, + "productCode": "PBITRISECI", + "name": "Bitrise Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16222/594536/icon/default.svg" + }, + { + "id": 20061, + "productCode": "PBREWBUNDLE", + "name": "Brew Bundle", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20061/666796/icon/default.svg" + }, + { + "id": 26344, + "productCode": "PBUGGREGATOR", + "name": "Buggregator", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/26344/698666/icon/default.svg" + }, + { + "id": 20985, + "productCode": "PBUILDMON", + "name": "Build Monitor", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/20985/452340/icon/default.svg" + }, + { + "id": 24568, + "productCode": "PCAMUNDASTARTER", + "name": "Camunda Starter", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24568/551328/icon/default.svg" + }, + { + "id": 17692, + "productCode": "PCAPELASTIC", + "name": "Cap-Elasticsearch", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17692/256059/icon/default.svg" + }, + { + "id": 17785, + "productCode": "PCAPREDIS", + "name": "Cap-Redis", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17785/256060/icon/default.svg" + }, + { + "id": 25810, + "productCode": "PLANGUAGEPACKTW", + "name": "Chinese (Traditional) Language Pack / IDE 本地化為繁體中文", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25810/635942/icon/default.svg" + }, + { + "id": 19114, + "productCode": "PCIINTG", + "name": "CIclone", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19114/632068/icon/default.svg" + }, + { + "id": 15458, + "productCode": "PCIRCLECI", + "name": "CircleCI Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15458/594537/icon/default.svg" + }, + { + "id": 22813, + "productCode": "PCITRIC", + "name": "Citric", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22813/684524/icon/default.svg" + }, + { + "id": 23887, + "productCode": "PCLAI", + "name": "CLAi", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23887/519870/icon/default.svg" + }, + { + "id": 26638, + "productCode": "PCLIPPS", + "name": "ClippS", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26638/690498/icon/default.svg" + }, + { + "id": 12869, + "productCode": "PCMAKEPLUS", + "name": "CMake Plus", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12869/671553/icon/default.svg" + }, + { + "id": 17501, + "productCode": "PISCRATCH", + "name": "Code Notes:Project Notes, Visual Markdown Editor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17501/682878/icon/default.svg" + }, + { + "id": 18394, + "productCode": "PCODEREFACTORAI", + "name": "Code Refactor AI", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18394/153532/icon/default.svg" + }, + { + "id": 14896, + "productCode": "PCWMP", + "name": "Code With Me", + "pricingModel": "FREEMIUM" + }, + { + "id": 19097, + "productCode": "PWGCODECREATOR", + "name": "codeCreator", + "pricingModel": "FREEMIUM" + }, + { + "id": 19578, + "productCode": "PCODEKITS", + "name": "CodeKits", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19578/302019/icon/default.svg" + }, + { + "id": 21314, + "productCode": "PCHATGPTCODING", + "name": "CodeMate", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21314/682167/icon/default.svg" + }, + { + "id": 10811, + "productCode": "PCODEMRBASE", + "name": "CodeMR", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/10811/585540/icon/default.svg" + }, + { + "id": 24958, + "productCode": "PCODEQL", + "name": "CodeQL", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24958/684477/icon/default.svg" + }, + { + "id": 24753, + "productCode": "PAAAA", + "name": "Combine and Copy Files to Clipboard For AI and LLM", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24753/667150/icon/default.svg" + }, + { + "id": 14104, + "productCode": "PVCS", + "name": "commit-template", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14104/250600/icon/default.svg" + }, + { + "id": 25716, + "productCode": "PCOMPOSEPREVIEW", + "name": "Compose Preview Generator", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/25716/672162/icon/default.svg" + }, + { + "id": 20293, + "productCode": "PCONNECTUI", + "name": "Connect Api", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/20293/452254/icon/default.svg" + }, + { + "id": 26736, + "productCode": "PCONSOLELOGPRO", + "name": "Console Log Pro", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/26736/694373/icon/default.svg" + }, + { + "id": 26716, + "productCode": "PCSVCOLORPRO", + "name": "CSV Color Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26716/690176/icon/default.svg" + }, + { + "id": 21857, + "productCode": "PCUEFY", + "name": "Cuefy", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21857/522713/icon/default.svg" + }, + { + "id": 13819, + "productCode": "PCYPRESSSUPPORT", + "name": "Cypresso", + "pricingModel": "FREEMIUM" + }, + { + "id": 25811, + "productCode": "PLANGUAGEPACKCS", + "name": "Czech Language Pack / Lokalizace IDE Do Češtiny", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25811/635931/icon/default.svg" + }, + { + "id": 24593, + "productCode": "PDAOGENERATOR", + "name": "DaoGenerator", + "pricingModel": "PAID" + }, + { + "id": 16861, + "productCode": "PDATABASE", + "name": "Database Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16861/610054/icon/default.svg" + }, + { + "id": 19161, + "productCode": "PDBDATABASETOOL", + "name": "Database Tool", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19161/681979/icon/default.svg" + }, + { + "id": 22472, + "productCode": "PDATAGRAPH", + "name": "DataGraph", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22472/668877/icon/default.svg" + }, + { + "id": 14371, + "productCode": "PIMAGEVIEWER", + "name": "Debug Image Viewer", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/14371/681721/icon/default.svg" + }, + { + "id": 24543, + "productCode": "PLANGUAGEPACKDE", + "name": "Deutsch (German) Language Pack / Deutsches Sprachpaket", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24543/635932/icon/default.svg" + }, + { + "id": 25496, + "productCode": "PDOCGENIE", + "name": "DocGenie", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25496/627176/icon/default.svg" + }, + { + "id": 11462, + "productCode": "DPN", + "name": "dotTrace", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/11462/81835/icon/default.svg" + }, + { + "id": 22884, + "productCode": "PDRYPUSH", + "name": "DryPush: AWS | GCP | Alibaba Deploy & Upload", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22884/699484/icon/default.svg" + }, + { + "id": 26476, + "productCode": "PDSL", + "name": "Dsl", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26476/695179/icon/default.svg" + }, + { + "id": 18896, + "productCode": "PDYNAMODB", + "name": "DynamoDB", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18896/619265/icon/default.svg" + }, + { + "id": 13847, + "productCode": "PEASYCODEMYBATI", + "name": "EasyCode-MybatisCodeHelper", + "pricingModel": "FREEMIUM" + }, + { + "id": 24396, + "productCode": "PEDITORASSISTFO", + "name": "Editor Assist For Code", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24396/621646/icon/default.svg" + }, + { + "id": 14512, + "productCode": "PELASTICSEARCH", + "name": "Elasticsearch", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14512/671231/icon/default.svg" + }, + { + "id": 18209, + "productCode": "PELSA", + "name": "ElasticSearch-Admin", + "pricingModel": "PAID" + }, + { + "id": 18663, + "productCode": "PEXCELEDITOR", + "name": "ExcelEditor", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/18663/633838/icon/default.svg" + }, + { + "id": 15379, + "productCode": "PGODRUNNER", + "name": "Execution God Recorder", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15379/102175/icon/default.svg" + }, + { + "id": 20238, + "productCode": "PEXTENSION", + "name": "Extensions Manager", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20238/308627/icon/default.svg" + }, + { + "id": 11058, + "productCode": "PEXTRAICONS", + "name": "Extra Icons", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/11058/699177/icon/default.svg" + }, + { + "id": 23927, + "productCode": "PEXTRAIDETWEAKS", + "name": "Extra IDE Tweaks", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23927/699176/icon/default.svg" + }, + { + "id": 24559, + "productCode": "PEXTPACK", + "name": "Extra Tools Pack", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24559/699180/icon/default.svg" + }, + { + "id": 16604, + "productCode": "PEXTRATOOLWINDO", + "name": "Extra ToolWindow Colorful Icons", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/16604/699178/icon/default.svg" + }, + { + "id": 26404, + "productCode": "PEXTRATCILIF", + "name": "Extra ToolWindow Colorful Icons Lifetime", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26404/699179/icon/default.svg" + }, + { + "id": 16988, + "productCode": "PFASTREQUEST", + "name": "Fast Request – API Buddy", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16988/695634/icon/default.svg" + }, + { + "id": 18971, + "productCode": "PFASTSHELL", + "name": "FastShell", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/18971/200999/icon/default.svg" + }, + { + "id": 23146, + "productCode": "PFEIGNHELPER", + "name": "Feign-Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23146/648950/icon/default.svg" + }, + { + "id": 16217, + "productCode": "PFUZYFIPC", + "name": "Find In Files (Favorites)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16217/637973/icon/default.svg" + }, + { + "id": 23609, + "productCode": "PFIREBASE", + "name": "Firebase Firestore", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23609/690270/icon/default.svg" + }, + { + "id": 15189, + "productCode": "PFIREHIGHLIGHT", + "name": "Firebase Rules", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15189/666806/icon/default.svg" + }, + { + "id": 23685, + "productCode": "PSCIPIOFTL", + "name": "Flexible Freemarker", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23685/670077/icon/default.svg" + }, + { + "id": 25912, + "productCode": "PFLINKSQLHELPER", + "name": "Flink SQL Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25912/674689/icon/default.svg" + }, + { + "id": 14718, + "productCode": "PFLUTTER", + "name": "Flutter Storm", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14718/522809/icon/default.svg" + }, + { + "id": 24539, + "productCode": "PLANGUAGEPACKFR", + "name": "French Language Pack / Pack De Langue Français", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24539/635946/icon/default.svg" + }, + { + "id": 26734, + "productCode": "PLOCALIZATIONFR", + "name": "French Language Pack – Français", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26734/690611/icon/default.svg" + }, + { + "id": 13086, + "productCode": "PGDOC", + "name": "Generate Document", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13086/659466/icon/default.svg" + }, + { + "id": 22971, + "productCode": "PGENSETANDSET", + "name": "GenerateSetAndGet", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22971/610911/icon/default.svg" + }, + { + "id": 16361, + "productCode": "PGENERATORCRUD", + "name": "Generator Crud", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16361/636729/icon/default.svg" + }, + { + "id": 26792, + "productCode": "PLOCALIZATIONDE", + "name": "German Language Pack – Deutsch", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26792/695550/icon/default.svg" + }, + { + "id": 20319, + "productCode": "PGERRYAURORA", + "name": "Gerry Aurora", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20319/435418/icon/default.svg" + }, + { + "id": 20246, + "productCode": "PGERRYCHERRY", + "name": "Gerry Cherry", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20246/435417/icon/default.svg" + }, + { + "id": 20247, + "productCode": "PGERRYCOFFEE", + "name": "Gerry Coffee", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20247/435420/icon/default.svg" + }, + { + "id": 20049, + "productCode": "PGERRYCYBERPUNK", + "name": "Gerry Cyberpunk", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20049/435415/icon/default.svg" + }, + { + "id": 20236, + "productCode": "PGERRYNATURE", + "name": "Gerry Nature", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20236/435419/icon/default.svg" + }, + { + "id": 20075, + "productCode": "PGERRYSPACE", + "name": "Gerry Space", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20075/435416/icon/default.svg" + }, + { + "id": 19668, + "productCode": "PGERRYTHEMESPRO", + "name": "Gerry Themes Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19668/694218/icon/default.svg" + }, + { + "id": 26472, + "productCode": "PGERRYTHEMESLT", + "name": "Gerry Themes Pro (Lifetime)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26472/694219/icon/default.svg" + }, + { + "id": 15457, + "productCode": "PGITLABCI", + "name": "Git CI Pipeline Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15457/627899/icon/default.svg" + }, + { + "id": 10083, + "productCode": "PGITSCOPE", + "name": "Git Scope", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/10083/396420/icon/default.svg" + }, + { + "id": 23813, + "productCode": "PGITWORKTREE", + "name": "Git Worktree", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/23813/698992/icon/default.svg" + }, + { + "id": 14056, + "productCode": "PGITFLOWPLUS", + "name": "GitFlowPlus", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/14056/678064/icon/default.svg" + }, + { + "id": 19347, + "productCode": "PGHACTNSMGRPRO", + "name": "GitHub Actions Manager", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19347/670488/icon/default.svg" + }, + { + "id": 20144, + "productCode": "PGITHUBCI", + "name": "Github CI Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20144/594538/icon/default.svg" + }, + { + "id": 22202, + "productCode": "PGITLABCICD", + "name": "GitLab CICD - Pipelines & Jobs, Builds Run Cancel Retry View Log", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22202/633878/icon/default.svg" + }, + { + "id": 26717, + "productCode": "PGITLABEVAREDIT", + "name": "GitLab Environment Variables Manager", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26717/691909/icon/default.svg" + }, + { + "id": 18689, + "productCode": "PGITLAB", + "name": "GitLab Integration Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18689/680758/icon/default.svg" + }, + { + "id": 20347, + "productCode": "PGITLABMASTER", + "name": "GitLab Master", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20347/634287/icon/default.svg" + }, + { + "id": 26095, + "productCode": "PGITLABMULTIREP", + "name": "GitLab Multi-Repo Search", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26095/655605/icon/default.svg" + }, + { + "id": 7499, + "productCode": "PGITTOOLBOX", + "name": "GitToolBox", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/7499/690185/icon/default.svg" + }, + { + "id": 19906, + "productCode": "PGOPARSER", + "name": "GoParser", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19906/293012/icon/default.svg" + }, + { + "id": 20411, + "productCode": "PWXUFQYRHZCRSEO", + "name": "Gorm", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20411/634455/icon/default.svg" + }, + { + "id": 22035, + "productCode": "PGPTASSISTANT", + "name": "GPT Assistant", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22035/696515/icon/default.svg" + }, + { + "id": 16136, + "productCode": "GZL", + "name": "Grazie Pro", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/16136/699463/icon/default.svg" + }, + { + "id": 15535, + "productCode": "PHEROKU", + "name": "Heroku Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15535/594541/icon/default.svg" + }, + { + "id": 24844, + "productCode": "PHTMXPRO", + "name": "HTMX Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24844/681998/icon/default.svg" + }, + { + "id": 26436, + "productCode": "PHTMXPROLIFETIM", + "name": "HTMX Pro (Lifetime)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26436/682000/icon/default.svg" + }, + { + "id": 25814, + "productCode": "PLANGUAGEPACKHU", + "name": "Hungarian Language Pack / Magyar Nyelvi Csomag", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25814/635936/icon/default.svg" + }, + { + "id": 7525, + "productCode": "PHYBRISCOMMERCE", + "name": "Hybris Integration", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/7525/640160/icon/default.svg" + }, + { + "id": 24053, + "productCode": "PPLUGDEVKITM", + "name": "IDE Plug DevKit Maven", + "pricingModel": "PAID" + }, + { + "id": 12634, + "productCode": "PIEDIS", + "name": "Iedis 2", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12634/167816/icon/default.svg" + }, + { + "id": 22459, + "productCode": "PIMAGETOVECTOR", + "name": "ImageToVector", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22459/376877/icon/default.svg" + }, + { + "id": 23859, + "productCode": "PINTELLIPHP", + "name": "IntelliPHP - AI Autocomplete for PHP", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23859/606769/icon/default.svg" + }, + { + "id": 20526, + "productCode": "PWAUFKYVHQCRXEO", + "name": "IoGame", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20526/468605/icon/default.svg" + }, + { + "id": 26785, + "productCode": "PIREPORTSIX", + "name": "IReport 6", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26785/695805/icon/default.svg" + }, + { + "id": 24494, + "productCode": "PLANGUAGEPACKIT", + "name": "Italian Language Pack / Pacchetto Lingua Italiano", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24494/635947/icon/default.svg" + }, + { + "id": 11560, + "productCode": "PBISJ", + "name": "Java Antidecompiler", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/11560/367010/icon/default.svg" + }, + { + "id": 10828, + "productCode": "PJDCLEANREAD", + "name": "JavaDoc Clean Read", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/10828/375796/icon/default.svg" + }, + { + "id": 20888, + "productCode": "PWXUQQYVOXCRSEO", + "name": "JavaKit", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20888/635381/icon/default.svg" + }, + { + "id": 14557, + "productCode": "PVISUALGC", + "name": "JDK VisualGC", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/14557/697775/icon/default.svg" + }, + { + "id": 24505, + "productCode": "PJEKADEV", + "name": "JeKa", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24505/666036/icon/default.svg" + }, + { + "id": 26270, + "productCode": "PJENKINSFILE", + "name": "Jenkinsfile", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26270/679717/icon/default.svg" + }, + { + "id": 26405, + "productCode": "PJENKINSLINTER", + "name": "Jenkinsfile Linter Offline", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26405/676491/icon/default.svg" + }, + { + "id": 26583, + "productCode": "PJENKINSFPRO", + "name": "Jenkinsfile Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26583/690288/icon/default.svg" + }, + { + "id": 22282, + "productCode": "AIP", + "name": "JetBrains AI Assistant", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22282/698410/icon/default.svg" + }, + { + "id": 21173, + "productCode": "PJETCLIENT", + "name": "JetClient - The Ultimate REST Client", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/21173/698391/icon/default.svg" + }, + { + "id": 9238, + "productCode": "PJETFORCER", + "name": "JetForcer | The Smartest Force.com IDE", + "pricingModel": "PAID" + }, + { + "id": 12621, + "productCode": "PJFORMDESIGNER", + "name": "JFormDesigner (Marketplace Edition)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12621/585119/icon/default.svg" + }, + { + "id": 23812, + "productCode": "PJMETERPLUGINSM", + "name": "JMeter Manager", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23812/668418/icon/default.svg" + }, + { + "id": 23855, + "productCode": "PJMETERRUNNER", + "name": "JMeter Runner", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23855/668416/icon/default.svg" + }, + { + "id": 22087, + "productCode": "PJMETERVIEWER", + "name": "JMeter Viewer", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22087/667442/icon/default.svg" + }, + { + "id": 15242, + "productCode": "PJPASQL", + "name": "JPA SQL", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/15242/522748/icon/default.svg" + }, + { + "id": 23360, + "productCode": "PJQEXPRESS", + "name": "jqExpress", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23360/635358/icon/default.svg" + }, + { + "id": 20013, + "productCode": "PJSONBROWSER", + "name": "JSON Browser", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20013/610491/icon/default.svg" + }, + { + "id": 15064, + "productCode": "PJSONKEYFINDER", + "name": "JSON Key Finder", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15064/669783/icon/default.svg" + }, + { + "id": 22597, + "productCode": "POXYJSONSCHGEN", + "name": "JSON Schema Generator", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22597/627717/icon/default.svg" + }, + { + "id": 23554, + "productCode": "POXYJSONDIAGRAM", + "name": "JSON Schema Visualizer/Editor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23554/629335/icon/default.svg" + }, + { + "id": 20297, + "productCode": "POXYJSONCONVERT", + "name": "JSON-YAML-XML Converter", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20297/629842/icon/default.svg" + }, + { + "id": 18975, + "productCode": "PJSONNETEMLSUP", + "name": "Jsonnet Pro", + "pricingModel": "PAID" + }, + { + "id": 19297, + "productCode": "PJSONTOANYLANGU", + "name": "JsonToAnyLanguage", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19297/351202/icon/default.svg" + }, + { + "id": 24694, + "productCode": "PJTRACKER", + "name": "JTracker: MyBatis Log & JPA Log", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24694/567175/icon/default.svg" + }, + { + "id": 16141, + "productCode": "PKSIX", + "name": "k6", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/16141/650126/icon/default.svg" + }, + { + "id": 22284, + "productCode": "PKAFKA", + "name": "Kafka Client", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22284/695617/icon/default.svg" + }, + { + "id": 20111, + "productCode": "PKAFKAIDE", + "name": "Kafkaide", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20111/425515/icon/default.svg" + }, + { + "id": 25083, + "productCode": "PKOTLINPARAMETE", + "name": "Kotlin Parameter Object", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25083/613913/icon/default.svg" + }, + { + "id": 18286, + "productCode": "PKSEXPLORER", + "name": "KS-Explorer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18286/666586/icon/default.svg" + }, + { + "id": 13441, + "productCode": "PLARAVEL", + "name": "Laravel Idea", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13441/686550/icon/default.svg" + }, + { + "id": 19661, + "productCode": "PLATTEPRO", + "name": "Latte Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19661/650159/icon/default.svg" + }, + { + "id": 15405, + "productCode": "PLEDGER", + "name": "LedgerPlugin", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15405/635196/icon/default.svg" + }, + { + "id": 17166, + "productCode": "PLEP", + "name": "LeetCode Editor Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17166/585832/icon/default.svg" + }, + { + "id": 24979, + "productCode": "PLOCALESPHERE", + "name": "Locale Sphere", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24979/636127/icon/default.svg" + }, + { + "id": 22223, + "productCode": "PLOCALSTACK", + "name": "LocalStack Toolkit", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22223/674818/icon/default.svg" + }, + { + "id": 20554, + "productCode": "PMAGE", + "name": "Magento and Adobe Commerce PhpStorm by Atwix", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/20554/668589/icon/default.svg" + }, + { + "id": 23556, + "productCode": "PSCIPIOMGNL", + "name": "Magnolia CMS Integration", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23556/670078/icon/default.svg" + }, + { + "id": 17688, + "productCode": "PRSMGNL", + "name": "Magnolia YAML Assistant", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/17688/666561/icon/default.svg" + }, + { + "id": 25250, + "productCode": "PMANAGEPROJECTS", + "name": "Manage Projects", + "pricingModel": "PAID" + }, + { + "id": 26084, + "productCode": "PMARKDTASK", + "name": "MarkdTask", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26084/680234/icon/default.svg" + }, + { + "id": 26080, + "productCode": "PMARVIN", + "name": "Marvin", + "pricingModel": "PAID" + }, + { + "id": 8006, + "productCode": "PMATERIALUI", + "name": "Material Theme UI", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/8006/678397/icon/default.svg" + }, + { + "id": 19308, + "productCode": "PMATERIALCUSTOM", + "name": "Material Theme UI Custom Theme", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19308/384272/icon/default.svg" + }, + { + "id": 19250, + "productCode": "PMATERIALEXTRAS", + "name": "Material Theme UI Extras", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19250/384271/icon/default.svg" + }, + { + "id": 17456, + "productCode": "PMATERIALHC", + "name": "Material Theme UI High Contrast", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17456/384270/icon/default.svg" + }, + { + "id": 19309, + "productCode": "PMATERIALLANG", + "name": "Material Theme UI Language Additions", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19309/384269/icon/default.svg" + }, + { + "id": 19310, + "productCode": "PMATERIALFRAME", + "name": "Material Theme UI Project Frame", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19310/385745/icon/default.svg" + }, + { + "id": 26139, + "productCode": "POZMAVENPLUGINX", + "name": "Maven Plug-in Extensions", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/26139/671003/icon/default.svg" + }, + { + "id": 24111, + "productCode": "PMAVENSEARCHER", + "name": "MavenSearcher", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24111/678940/icon/default.svg" + }, + { + "id": 13615, + "productCode": "PMRINTEGEE", + "name": "Merge Request Integration EE - Code Review for GitLab", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13615/113000/icon/default.svg" + }, + { + "id": 23687, + "productCode": "PMICRONAUTLAUNC", + "name": "Micronaut Launch", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23687/478379/icon/default.svg" + }, + { + "id": 13720, + "productCode": "PMINBATIS", + "name": "MinBatis", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13720/116149/icon/default.svg" + }, + { + "id": 23999, + "productCode": "PMONGODB", + "name": "Mongo DB", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23999/615626/icon/default.svg" + }, + { + "id": 20761, + "productCode": "PMONGOEXPERT", + "name": "MongoExpert", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20761/668205/icon/default.svg" + }, + { + "id": 13643, + "productCode": "PMONOKAIPRO", + "name": "Monokai Pro Theme", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/13643/672488/icon/default.svg" + }, + { + "id": 17465, + "productCode": "PCDMQTTCLIENT", + "name": "MQTT Client", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/17465/308433/icon/default.svg" + }, + { + "id": 25259, + "productCode": "PMYBATISDAOGEN", + "name": "MyBatis Dao Generator", + "pricingModel": "PAID" + }, + { + "id": 13905, + "productCode": "PMYBATISLOG", + "name": "MyBatis Log", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13905/617368/icon/default.svg" + }, + { + "id": 26550, + "productCode": "PMYBATISLOGULTR", + "name": "Mybatis Log Ultra", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26550/699611/icon/default.svg" + }, + { + "id": 23478, + "productCode": "PMYBATISCODE", + "name": "MyBatis Plus Tools", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23478/628011/icon/default.svg" + }, + { + "id": 18389, + "productCode": "PMBCODEHELPPRO", + "name": "Mybatis Smart Code Help Pro", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/18389/681996/icon/default.svg" + }, + { + "id": 14522, + "productCode": "PMYBATISHELPER", + "name": "MyBatisCodeHelperPro (Marketplace Edition)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14522/664510/icon/default.svg" + }, + { + "id": 25549, + "productCode": "PMYBATISTOOLS", + "name": "MybatisTools", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25549/635036/icon/default.svg" + }, + { + "id": 22655, + "productCode": "PMYSQLPROXY", + "name": "MySQL Proxy", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22655/542161/icon/default.svg" + }, + { + "id": 18387, + "productCode": "PNEONPRO", + "name": "NEON Nette Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18387/666037/icon/default.svg" + }, + { + "id": 19977, + "productCode": "PNETLIFY", + "name": "Netlify Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19977/594542/icon/default.svg" + }, + { + "id": 19963, + "productCode": "PNEXTSKETCH", + "name": "NextSketch", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19963/272018/icon/default.svg" + }, + { + "id": 20805, + "productCode": "PNEXTSKETCHTWO", + "name": "NextSketch2", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20805/308349/icon/default.svg" + }, + { + "id": 23765, + "productCode": "PNFLUTTER", + "name": "NFlutter", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23765/492994/icon/default.svg" + }, + { + "id": 18280, + "productCode": "PNGINX", + "name": "Nginx Configuration Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18280/699005/icon/default.svg" + }, + { + "id": 19205, + "productCode": "PNGROK", + "name": "Ngrok", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19205/666812/icon/default.svg" + }, + { + "id": 21833, + "productCode": "PNOSQLNAVMDB", + "name": "NoSQL Navigator For MongoDB", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21833/664767/icon/default.svg" + }, + { + "id": 22128, + "productCode": "PNPMPACKAGEJSON", + "name": "NPM Package Json", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22128/594555/icon/default.svg" + }, + { + "id": 13499, + "productCode": "PODOO", + "name": "Odoo", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13499/696778/icon/default.svg" + }, + { + "id": 13151, + "productCode": "POFFICEFLOOR", + "name": "OfficeFloor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13151/131867/icon/default.svg" + }, + { + "id": 21410, + "productCode": "PONEGAICOPILOT", + "name": "Onegai Copilot", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/21410/693603/icon/default.svg" + }, + { + "id": 19889, + "productCode": "POPENAPICRUDWIZ", + "name": "OpenAPI CRUD Wizard", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19889/478359/icon/default.svg" + }, + { + "id": 12887, + "productCode": "POPENAPI", + "name": "OpenAPI Editor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12887/658050/icon/default.svg" + }, + { + "id": 12626, + "productCode": "PORCHIDE", + "name": "OrchidE - Ansible Language Support", + "pricingModel": "PAID" + }, + { + "id": 21353, + "productCode": "POWASPIDEVULSCA", + "name": "OWASP IDE-VulScanner", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/21353/584074/icon/default.svg" + }, + { + "id": 17440, + "productCode": "PHPBUILDER", + "name": "PHP Data Object Generator", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/17440/458368/icon/default.svg" + }, + { + "id": 18981, + "productCode": "PPHPHOUDINI", + "name": "PHP Houdini", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18981/271233/icon/default.svg" + }, + { + "id": 16935, + "productCode": "PHPEAPLUGIN", + "name": "Php Inspections (EA Ultimate)", + "pricingModel": "PAID" + }, + { + "id": 23869, + "productCode": "PPKXCODEGENERAT", + "name": "PKXCodeGenerator", + "pricingModel": "PAID" + }, + { + "id": 14821, + "productCode": "PPUMLSTUDIO", + "name": "PlantUML Studio", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14821/172312/icon/default.svg" + }, + { + "id": 13733, + "productCode": "PPOJOTOJSONSCH", + "name": "POJO to JSON Schema", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13733/95154/icon/default.svg" + }, + { + "id": 22429, + "productCode": "PPOLARISTOMCATS", + "name": "Polaris Tomcat Server", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22429/555142/icon/default.svg" + }, + { + "id": 25812, + "productCode": "PLANGUAGEPACKPL", + "name": "Polish Language Pack / Lokalizacja IDE Na Język Polski", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25812/635934/icon/default.svg" + }, + { + "id": 21361, + "productCode": "POLYBPMNGDNEXT", + "name": "PolyBPMN visualizer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21361/592087/icon/default.svg" + }, + { + "id": 24542, + "productCode": "PLANGUAGEPACKPT", + "name": "Portuguese Language Pack / Pacote De Idioma Português", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24542/635943/icon/default.svg" + }, + { + "id": 26724, + "productCode": "PLOCALIZATIONPT", + "name": "Portuguese Language Pack – Português", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26724/690241/icon/default.svg" + }, + { + "id": 25582, + "productCode": "PPRFLOWTRACKER", + "name": "PR-FlowTracker", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25582/628502/icon/default.svg" + }, + { + "id": 25326, + "productCode": "PPRSNAPVIEW", + "name": "PR-SnapView", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25326/604773/icon/default.svg" + }, + { + "id": 14434, + "productCode": "PQMLEDITOR", + "name": "QmlEditor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14434/314029/icon/default.svg" + }, + { + "id": 16405, + "productCode": "PQTSQSSEDITOR", + "name": "Qt Style Sheets Editor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16405/193379/icon/default.svg" + }, + { + "id": 19027, + "productCode": "PQUARKUSHELPER", + "name": "Quarkus Assistant", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19027/477162/icon/default.svg" + }, + { + "id": 18269, + "productCode": "PQUERYFLAG", + "name": "QueryFlag", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18269/633474/icon/default.svg" + }, + { + "id": 26348, + "productCode": "PCODETOOLKIT", + "name": "Quickly-Code-Toolkit", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26348/678215/icon/default.svg" + }, + { + "id": 10080, + "productCode": "PRAINBOWBRACKET", + "name": "Rainbow Brackets", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/10080/689210/icon/default.svg" + }, + { + "id": 19316, + "productCode": "PRANCHER", + "name": "Rancher", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19316/656524/icon/default.svg" + }, + { + "id": 26218, + "productCode": "PRCHSPRINGCODEG", + "name": "rch-spring-code-generation", + "pricingModel": "PAID" + }, + { + "id": 13838, + "productCode": "PRDFANDSPARQL", + "name": "RDF and SPARQL", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13838/699002/icon/default.svg" + }, + { + "id": 9564, + "productCode": "PRNCONSOLE", + "name": "React Native Console", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/9564/697769/icon/default.svg" + }, + { + "id": 12820, + "productCode": "PREDIS", + "name": "Redis", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12820/666089/icon/default.svg" + }, + { + "id": 19360, + "productCode": "PREDISCLIHELPER", + "name": "Redis Client", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19360/612259/icon/default.svg" + }, + { + "id": 15722, + "productCode": "PREDISMANAGER", + "name": "Redis Manager", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15722/270538/icon/default.svg" + }, + { + "id": 19599, + "productCode": "PREDISS", + "name": "Redis Operator", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19599/558218/icon/default.svg" + }, + { + "id": 16160, + "productCode": "PREDISTOOLS", + "name": "Redis-Cli", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/16160/619509/icon/default.svg" + }, + { + "id": 15433, + "productCode": "PREGEXTOOL", + "name": "Regex Tool", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15433/521938/icon/default.svg" + }, + { + "id": 24265, + "productCode": "PDLXRJAS", + "name": "Resilience4j Annotation Support", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24265/598747/icon/default.svg" + }, + { + "id": 22726, + "productCode": "PRETROFITASSIT", + "name": "Retrofit Assistant", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22726/514538/icon/default.svg" + }, + { + "id": 24544, + "productCode": "PLANGUAGEPACKRU", + "name": "Russian Language Pack / Локализация IDE На Русский Язык", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24544/635933/icon/default.svg" + }, + { + "id": 26495, + "productCode": "PLOCALIZATIONRU", + "name": "Russian Language Pack – Русский Язык", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26495/684251/icon/default.svg" + }, + { + "id": 22407, + "productCode": "PRR", + "name": "Rust", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22407/698058/icon/default.svg" + }, + { + "id": 22428, + "productCode": "PWXUQRYTOXCRSEO", + "name": "RustTool", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22428/634725/icon/default.svg" + }, + { + "id": 13668, + "productCode": "PSFCC", + "name": "Salesforce B2C Commerce (SFCC)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/13668/634476/icon/default.svg" + }, + { + "id": 22748, + "productCode": "PSCHEMAREGVIEW", + "name": "Schema Registry Viewer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22748/496140/icon/default.svg" + }, + { + "id": 12108, + "productCode": "PSCIPIO", + "name": "Scipio ERP Integration", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/12108/692856/icon/default.svg" + }, + { + "id": 19556, + "productCode": "PSCREENCODEPRO", + "name": "ScreenCodePro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19556/198086/icon/default.svg" + }, + { + "id": 22232, + "productCode": "PSENTRY", + "name": "Sentry", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22232/668882/icon/default.svg" + }, + { + "id": 15945, + "productCode": "PSENTRYINTEG", + "name": "Sentry Integration", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15945/111527/icon/default.svg" + }, + { + "id": 25807, + "productCode": "PSEQUENCECPP", + "name": "SequenceDiagram C/C++", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25807/698045/icon/default.svg" + }, + { + "id": 8286, + "productCode": "PSEQUENCEDIAGRA", + "name": "SequenceDiagram Core", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/8286/667138/icon/default.svg" + }, + { + "id": 25623, + "productCode": "PSEQUENCEGO", + "name": "SequenceDiagram Go", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25623/686539/icon/default.svg" + }, + { + "id": 24973, + "productCode": "PSEQUENCEJS", + "name": "SequenceDiagram JS", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24973/686111/icon/default.svg" + }, + { + "id": 25978, + "productCode": "PSEQUENCEPYTHON", + "name": "SequenceDiagram Python", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25978/689195/icon/default.svg" + }, + { + "id": 25376, + "productCode": "PSEQUENCERUST", + "name": "SequenceDiagram Rust", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25376/682159/icon/default.svg" + }, + { + "id": 23115, + "productCode": "PSEQDIAORG", + "name": "SequenceDiagram.org", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23115/675161/icon/default.svg" + }, + { + "id": 19727, + "productCode": "PSEQUENCEOUTLIN", + "name": "SequenceOutline", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19727/561485/icon/default.svg" + }, + { + "id": 14437, + "productCode": "PSI", + "name": "Shared Project Indexes", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14437/692150/icon/default.svg" + }, + { + "id": 7410, + "productCode": "PSWPLUGIN", + "name": "Shopware", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/7410/586732/icon/default.svg" + }, + { + "id": 18151, + "productCode": "PBEANCONVERTER", + "name": "Simple Object Copy", + "pricingModel": "PAID" + }, + { + "id": 20243, + "productCode": "PSKOL", + "name": "Skol", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20243/667209/icon/default.svg" + }, + { + "id": 24754, + "productCode": "PSMARTCRYPTOR", + "name": "Smart Cryptor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24754/651506/icon/default.svg" + }, + { + "id": 25751, + "productCode": "PSMARTINPUT", + "name": "Smart Input Pro (Japan, South Korea, Russia & more)", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/25751/682487/icon/default.svg" + }, + { + "id": 14053, + "productCode": "PSMARTJUMP", + "name": "Smart Jump", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14053/274462/icon/default.svg" + }, + { + "id": 24555, + "productCode": "PSMARTTOMCATPRO", + "name": "Smart Tomcat Pro", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/24555/620387/icon/default.svg" + }, + { + "id": 24889, + "productCode": "PSNAPSHOTSFORAI", + "name": "Snapshots for AI", + "pricingModel": "PAID" + }, + { + "id": 20565, + "productCode": "PTAILWINDTOOLS", + "name": "Snippet Toolkit for Tailwind CSS", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20565/502104/icon/default.svg" + }, + { + "id": 23743, + "productCode": "PSOTERISECURITY", + "name": "Soteri Secret Scanner", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23743/587653/icon/default.svg" + }, + { + "id": 22318, + "productCode": "PSOURCESYNCPRO", + "name": "Source Synchronizer Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22318/503948/icon/default.svg" + }, + { + "id": 24541, + "productCode": "PLANGUAGEPACKES", + "name": "Spanish Language Pack / Paquete De Idioma Español", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24541/635935/icon/default.svg" + }, + { + "id": 26581, + "productCode": "PLOCALIZATIONES", + "name": "Spanish Language Pack – Español", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26581/684253/icon/default.svg" + }, + { + "id": 26079, + "productCode": "PSPARKSQLHELPER", + "name": "Spark SQL Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26079/671851/icon/default.svg" + }, + { + "id": 14338, + "productCode": "PSPARQL", + "name": "SPARQL", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/14338/165810/icon/default.svg" + }, + { + "id": 22635, + "productCode": "PSPEECHTOTEXT", + "name": "Speech-To-Text (AWS Transcribe)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22635/388024/icon/default.svg" + }, + { + "id": 18622, + "productCode": "PSPRINGBOOTIDEA", + "name": "Spring Boot Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18622/623187/icon/default.svg" + }, + { + "id": 22304, + "productCode": "PSRCODEGEN", + "name": "Spring Rest Code Generator", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22304/391163/icon/default.svg" + }, + { + "id": 15574, + "productCode": "PFLYINSKYZJBZ", + "name": "spring-assistant-@valueToYml", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15574/412050/icon/default.svg" + }, + { + "id": 25538, + "productCode": "PSQLCOP", + "name": "SQL COP", + "pricingModel": "PAID" + }, + { + "id": 19660, + "productCode": "PAWSQLADVISOR", + "name": "SQL Optimizer,Index Advisor MySQL/Postgre/Oracle, Tune by PawSQL", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19660/690227/icon/default.svg" + }, + { + "id": 24205, + "productCode": "PSQLDEBUGGER", + "name": "SQLDebugger", + "pricingModel": "PAID" + }, + { + "id": 20534, + "productCode": "PSQLFLUFFLINTER", + "name": "Sqlfluff Linter (Ultimate Edition)", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20534/666844/icon/default.svg" + }, + { + "id": 22742, + "productCode": "PSQLFORMATTER", + "name": "SQLFormatter", + "pricingModel": "PAID" + }, + { + "id": 22713, + "productCode": "PDBSSH", + "name": "SSH Tool", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/22713/681987/icon/default.svg" + }, + { + "id": 24625, + "productCode": "PKUOYGHHDS", + "name": "SSHMatrix", + "pricingModel": "PAID" + }, + { + "id": 15851, + "productCode": "PSTORMSECTIONS", + "name": "StormSections", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/15851/681043/icon/default.svg" + }, + { + "id": 14482, + "productCode": "PSTRKER", + "name": "Stryker", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/14482/134911/icon/default.svg" + }, + { + "id": 22150, + "productCode": "PSWIFTSUPPORT", + "name": "Swift Support", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22150/693948/icon/default.svg" + }, + { + "id": 23458, + "productCode": "PSWISSKITCONVER", + "name": "SwissKit Converter", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23458/538669/icon/default.svg" + }, + { + "id": 7219, + "productCode": "PSYMFONYPLUGIN", + "name": "Symfony Support", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/7219/605730/icon/default.svg" + }, + { + "id": 10695, + "productCode": "PVLOG", + "name": "SystemVerilog", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/10695/163626/icon/default.svg" + }, + { + "id": 26514, + "productCode": "PSYSTEMVERILOGL", + "name": "SystemVerilog Language Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26514/682287/icon/default.svg" + }, + { + "id": 14203, + "productCode": "PSVERILOG", + "name": "SystemVerilog Studio", + "pricingModel": "PAID" + }, + { + "id": 23937, + "productCode": "PTAILWINDFOLD", + "name": "Tailwind Fold", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/23937/688749/icon/default.svg" + }, + { + "id": 22685, + "productCode": "PNEKOCAT", + "name": "TamaCat Web Browser", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/22685/698234/icon/default.svg" + }, + { + "id": 18857, + "productCode": "PTERMINAL", + "name": "Terminal Pro", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/18857/699167/icon/default.svg" + }, + { + "id": 26761, + "productCode": "PTIMETRACKERX", + "name": "TimeTracker X", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26761/699452/icon/default.svg" + }, + { + "id": 20050, + "productCode": "PTLDRAI", + "name": "TLDR", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/20050/394140/icon/default.svg" + }, + { + "id": 14384, + "productCode": "PTOOLSET", + "name": "Toolset", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/14384/681853/icon/default.svg" + }, + { + "id": 20683, + "productCode": "PTRAVISCI", + "name": "Travis CI Dashboard", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/20683/594543/icon/default.svg" + }, + { + "id": 25813, + "productCode": "PLANGUAGEPACKTR", + "name": "Turkish Language Pack / IDE'nin Türkçe'ye Yerelleştirilmesi", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/25813/635938/icon/default.svg" + }, + { + "id": 18232, + "productCode": "PDJANGOTPLPEP", + "name": "Typed Django Template", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/18232/370564/icon/default.svg" + }, + { + "id": 19675, + "productCode": "PUNIAPPSUPPORT", + "name": "Uniapp Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/19675/625817/icon/default.svg" + }, + { + "id": 21813, + "productCode": "PVERILOGLANGUAG", + "name": "Verilog Language Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/21813/666097/icon/default.svg" + }, + { + "id": 26267, + "productCode": "PVHDLLANGUAGESU", + "name": "VHDL Language Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26267/671860/icon/default.svg" + }, + { + "id": 23086, + "productCode": "PVOQAL", + "name": "Voqal Coder", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23086/619943/icon/default.svg" + }, + { + "id": 24816, + "productCode": "PVSCODEICONS", + "name": "VSCode Icons", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24816/635813/icon/default.svg" + }, + { + "id": 18860, + "productCode": "PWIREMOCHA", + "name": "WireMocha", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18860/687614/icon/default.svg" + }, + { + "id": 7232, + "productCode": "PWLANG", + "name": "Wolfram Language", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/7232/650671/icon/default.svg" + }, + { + "id": 18425, + "productCode": "PXSDVISUALIZER", + "name": "XSD / WSDL Visualizer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/18425/695783/icon/default.svg" + }, + { + "id": 19024, + "productCode": "POXYXSDJSONSCH", + "name": "XSD to JSON Schema", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/19024/627712/icon/default.svg" + }, + { + "id": 9739, + "productCode": "PYAOQIANGBPMN", + "name": "Yaoqiang BPMN Editor", + "pricingModel": "PAID" + }, + { + "id": 23693, + "productCode": "PYIIFRAMEWORK", + "name": "Yii2 Framework Support", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/23693/671528/icon/default.svg" + }, + { + "id": 26463, + "productCode": "PYIMICODEGEN", + "name": "Yimi CodeGen", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26463/699631/icon/default.svg" + }, + { + "id": 12437, + "productCode": "PZENUML", + "name": "ZenUML Support", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/12437/656093/icon/default.svg" + }, + { + "id": 15773, + "productCode": "PZEROCODE", + "name": "Zerocode Scenario Helper", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/15773/155295/icon/default.svg" + }, + { + "id": 18341, + "productCode": "PZKA", + "name": "Zookeeper-Admin", + "pricingModel": "PAID" + }, + { + "id": 27802, + "productCode": "PJAVASINART", + "name": "AEM Repository Tools", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27802/794567/icon/default.svg" + }, + { + "id": 27573, + "productCode": "PAICODEEYE", + "name": "AI CodeEye", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/27573/801275/icon/default.svg" + }, + { + "id": 27843, + "productCode": "PALLICONSPACK", + "name": "All Icons Pack", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27843/811859/icon/default.svg" + }, + { + "id": 15673, + "productCode": "PASYNCAPI", + "name": "AsyncAPI", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/15673/807396/icon/default.svg" + }, + { + "id": 27750, + "productCode": "PAWESOMELOGVIEW", + "name": "Awesome Log Viewer", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/27750/798835/icon/default.svg" + }, + { + "id": 28076, + "productCode": "PBOOTSTRAPFOLD", + "name": "Bootstrap Fold", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/28076/815715/icon/default.svg" + }, + { + "id": 27209, + "productCode": "PCODEPORTAL", + "name": "Code Portal", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27209/752667/icon/default.svg" + }, + { + "id": 27027, + "productCode": "PDARTDATACLASS", + "name": "Dart Data Class Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27027/720981/icon/default.svg" + }, + { + "id": 28160, + "productCode": "PDIRENV", + "name": "DirEnv Pro", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28160/824875/icon/default.svg" + }, + { + "id": 13834, + "productCode": "PDJANGOCOMMANDR", + "name": "Django command runner", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/13834/816480/icon/default.svg" + }, + { + "id": 27023, + "productCode": "PDJANGOPOWERTOO", + "name": "Django PowerTools", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/27023/824864/icon/default.svg" + }, + { + "id": 27953, + "productCode": "PENVCLI", + "name": "EnvCLI", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27953/812454/icon/default.svg" + }, + { + "id": 27808, + "productCode": "PEXTRAIDETWLIFE", + "name": "Extra IDE Tweaks Lifetime", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27808/818725/icon/default.svg" + }, + { + "id": 27681, + "productCode": "PFILESMANAGER", + "name": "Files Manager for Claude", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27681/780409/icon/default.svg" + }, + { + "id": 26998, + "productCode": "PGITHUBACTIONSH", + "name": "GitHub Actions Helper", + "pricingModel": "FREEMIUM" + }, + { + "id": 27654, + "productCode": "PHUBLHERO", + "name": "HubL Hero", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27654/778283/icon/default.svg" + }, + { + "id": 27856, + "productCode": "PEGTRANSJVM", + "name": "i18n Translate Pro: JVM", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27856/808495/icon/default.svg" + }, + { + "id": 28020, + "productCode": "PEGTRANSWEB", + "name": "i18n Translate Pro: Web", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28020/813704/icon/default.svg" + }, + { + "id": 27283, + "productCode": "PKARATEGEN", + "name": "KarateGen", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27283/743186/icon/default.svg" + }, + { + "id": 27659, + "productCode": "PKUBERNETESCONT", + "name": "Kubernetes Control Center", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/27659/824540/icon/default.svg" + }, + { + "id": 27826, + "productCode": "PLIQUIDHERO", + "name": "Liquid Hero", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27826/786324/icon/default.svg" + }, + { + "id": 28071, + "productCode": "PMCPSERVERSFORJ", + "name": "MCP Servers for AI Assistants", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/28071/819609/icon/default.svg" + }, + { + "id": 27118, + "productCode": "POPENAPIGUIEDIT", + "name": "OpenAPI GUI Editor", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27118/827033/icon/default.svg" + }, + { + "id": 27222, + "productCode": "PPANDEVJIRAAUTO", + "name": "PanDev Jira Auto Worklog", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27222/757078/icon/default.svg" + }, + { + "id": 27219, + "productCode": "PPINESCRIPTSUPP", + "name": "Pine Script Support", + "pricingModel": "PAID" + }, + { + "id": 15640, + "productCode": "PPYTHONBYTECODE", + "name": "Python Bytecode", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/15640/812825/icon/default.svg" + }, + { + "id": 24142, + "productCode": "PRUSTX", + "name": "RustX", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/24142/819278/icon/default.svg" + }, + { + "id": 27936, + "productCode": "PSAVE", + "name": "Save", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27936/797247/icon/default.svg" + }, + { + "id": 28063, + "productCode": "PSERVERLESSIN", + "name": "Serverless Insight", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28063/817887/icon/default.svg" + }, + { + "id": 23694, + "productCode": "PSMARTHUGO", + "name": "Smart Hugo", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/23694/821572/icon/default.svg" + }, + { + "id": 27596, + "productCode": "PSMARTCHANGELIS", + "name": "SmartChangelist AI", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27596/799819/icon/default.svg" + }, + { + "id": 27756, + "productCode": "PSOURCECODEDOCG", + "name": "Source Code Doc Generation", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27756/796458/icon/default.svg" + }, + { + "id": 26854, + "productCode": "PSPRINGNAVI", + "name": "Spring Navigator", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/26854/740609/icon/default.svg" + }, + { + "id": 27279, + "productCode": "PSTACKTRACE", + "name": "StackTrace", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/27279/744820/icon/default.svg" + }, + { + "id": 27684, + "productCode": "PTHRIFTREQUEST", + "name": "Thrift Request", + "pricingModel": "FREEMIUM", + "icon": "https://plugins.jetbrains.com/files/27684/779963/icon/default.svg" + }, + { + "id": 28202, + "productCode": "PAPIBLUEPRINT", + "name": "API Blueprint", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28202/837499/icon/default.svg" + }, + { + "id": 28145, + "productCode": "PJSONLOGVIEWER", + "name": "Json Log Viewer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28145/825689/icon/default.svg" + }, + { + "id": 28142, + "productCode": "PMATERIALISLAND", + "name": "Material Theme UI Islands", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28142/822441/icon/default.svg" + }, + { + "id": 28143, + "productCode": "PMATERIALRANDOM", + "name": "Material Theme UI Theme Randomizer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28143/822443/icon/default.svg" + }, + { + "id": 28261, + "productCode": "PNETLINXTOOLS", + "name": "NetLinx Tools", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28261/838438/icon/default.svg" + }, + { + "id": 28259, + "productCode": "PHEALTHTIMER", + "name": "Pomodoro Healthy Exercise Timer", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28259/835984/icon/default.svg" + }, + { + "id": 28171, + "productCode": "PREDOSCHECKER", + "name": "ReDoSChecker", + "pricingModel": "PAID" + }, + { + "id": 28392, + "productCode": "PSYSTEMNOTIFIER", + "name": "System Notifier", + "pricingModel": "PAID", + "icon": "https://plugins.jetbrains.com/files/28392/851065/icon/default.svg" + } +] \ No newline at end of file diff --git a/src/main/resources/external/data/product.json b/src/main/resources/external/data/product.json new file mode 100644 index 0000000..2da26f1 --- /dev/null +++ b/src/main/resources/external/data/product.json @@ -0,0 +1,77 @@ +[ + { + "name": "All Jetbrains's Product Or Plugin", + "productCode": "", + "iconClass": "icon-al" + }, + { + "name": "IntelliJ IDEA", + "productCode": "II,PCWMP,PSI", + "iconClass": "icon-ii" + }, + { + "name": "PhpStorm", + "productCode": "PS,PCWMP,PSI", + "iconClass": "icon-ps" + }, + { + "name": "AppCode", + "productCode": "AC,PCWMP,PSI", + "iconClass": "icon-ac" + }, + { + "name": "DataGrip", + "productCode": "DB,PSI,PDB", + "iconClass": "icon-db" + }, + { + "name": "RubyMine", + "productCode": "RM,PCWMP,PSI", + "iconClass": "icon-rm" + }, + { + "name": "WebStorm", + "productCode": "WS,PCWMP,PSI", + "iconClass": "icon-ws" + }, + { + "name": "Rider", + "productCode": "RD,PDB,PSI,PCWMP", + "iconClass": "icon-rd" + }, + { + "name": "CLion", + "productCode": "CL,PSI,PCWMP", + "iconClass": "icon-cl" + }, + { + "name": "PyCharm", + "productCode": "PC,PSI,PCWMP", + "iconClass": "icon-pc" + }, + { + "name": "GoLand", + "productCode": "GO,PSI,PCWMP", + "iconClass": "icon-go" + }, + { + "name": "DataSpell", + "productCode": "DS,PSI,PDB,PCWMP", + "iconClass": "icon-ds" + }, + { + "name": "dotMemory", + "productCode": "DM", + "iconClass": "icon-dm" + }, + { + "name": "Aqua", + "productCode": "QA,PSI,PCWMP", + "iconClass": "icon-qa" + }, + { + "name": "RustRover", + "productCode": "RR,PSI,PCWMP", + "iconClass": "icon-rr" + } +] \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..08e1a23 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,121 @@ + + + + + + + + true + + + + %gray(%d{yyyy-MM-dd HH:mm:ss.SSS}) %magenta([%thread]) %highlight(%-5level) %cyan(%logger{36}) - %msg%n + UTF-8 + + + + + + + + ${LOG_HOME}/qiumo-help.%d{yyyy-MM-dd}.log + + 30 + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + 100MB + + + + + + + ERROR + ACCEPT + DENY + + + ${LOG_HOME}/error.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + 100MB + + + + + + + 0 + + 512 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 0000000..847503d --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,328 @@ +/* 全局样式 */ +body { + font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* 动画效果 */ +.fade-enter-active, .fade-leave-active { + transition: opacity 0.3s ease; +} + +.fade-enter-from, .fade-leave-to { + opacity: 0; +} + +/* 卡片悬停效果 */ +.card-hover { + transition: all 0.3s ease; +} + +.card-hover:hover { + transform: translateY(-4px); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); +} + +/* 按钮动画 */ +.btn-primary { + transition: all 0.2s ease; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +.btn-primary:active { + transform: translateY(0); +} + +/* 渐变背景 */ +.gradient-bg { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.gradient-text { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* 毛玻璃效果 */ +.glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* 加载动画 */ +.loading { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* 模态框动画 */ +.modal-enter-active, .modal-leave-active { + transition: all 0.3s ease; +} + +.modal-enter-from, .modal-leave-to { + opacity: 0; + transform: scale(0.9); +} + +/* 产品图标样式 */ +.product-icon { + width: 64px; + height: 64px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 激活码展示区域 */ +.license-code-display { + background: #1f2937; + color: #f9fafb; + border-radius: 8px; + padding: 16px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + word-break: break-all; + line-height: 1.6; + max-height: 300px; + overflow-y: auto; + font-size: 14px; +} + +/* 通知样式 */ +.notification { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 400px; + padding: 16px 20px; + border-radius: 8px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + animation: slideInRight 0.3s ease; +} + +.notification.success { + background: #10b981; + color: white; +} + +.notification.error { + background: #ef4444; + color: white; +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* 搜索框样式 */ +.search-container { + position: relative; +} + +.search-input { + padding-left: 44px; +} + +.search-icon { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: #9ca3af; + z-index: 1; + pointer-events: none; + font-size: 14px; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .license-code-display { + font-size: 12px; + padding: 12px; + max-height: 200px; + } + + .notification { + right: 10px; + left: 10px; + max-width: none; + } +} + +/* 返回顶部按钮 */ +.back-to-top { + position: fixed; + bottom: 30px; + right: 30px; + width: 50px; + height: 50px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease; + z-index: 1000; + opacity: 0; + transform: translateY(20px); + pointer-events: none; +} + +.back-to-top.show { + opacity: 1; + transform: translateY(0); + pointer-events: all; +} + +.back-to-top:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +@media (max-width: 768px) { + .back-to-top { + bottom: 90px; /* 避开底部导航栏 */ + right: 20px; + width: 45px; + height: 45px; + } +} + +/* JRebel页面特殊样式 */ +.jrebel-container { + overflow-x: auto; +} + +.jrebel-step { + min-width: 0; + word-wrap: break-word; +} + +/* 配置说明区域 */ +.config-section { + background: #f8fafc; + border-left: 4px solid #3b82f6; + padding: 16px; + margin: 16px 0; + border-radius: 0 8px 8px 0; +} + +.config-code { + background: #1f2937; + color: #f9fafb; + padding: 12px; + border-radius: 6px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; + white-space: pre-wrap; + overflow-x: auto; + word-break: break-all; +} + +/* 配置步骤优化 */ +.config-step { + word-wrap: break-word; + overflow-wrap: break-word; +} + +.config-step-content { + min-width: 0; + flex: 1; +} + +/* 移动端优化 */ +@media (max-width: 768px) { + .license-code-display { + font-size: 12px; + padding: 12px; + max-height: 200px; + } + + .notification { + right: 10px; + left: 10px; + max-width: none; + } + + .config-code { + font-size: 11px; + padding: 8px; + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: break-word; + } + + .config-section { + padding: 12px; + margin: 12px 0; + } + + /* 配置步骤移动端优化 */ + .config-step-mobile { + flex-direction: column; + space-y: 12px; + } + + .config-step-mobile .config-step-content { + margin-left: 0; + margin-top: 8px; + } + + /* 首页内容移动端优化 */ + .home-section { + padding: 16px; + margin-bottom: 16px; + } + + .home-title { + font-size: 28px; + line-height: 1.2; + } + + .home-subtitle { + font-size: 16px; + line-height: 1.4; + } +} \ No newline at end of file diff --git a/src/main/resources/static/images/aqua.svg b/src/main/resources/static/images/aqua.svg new file mode 100644 index 0000000..f5c40b9 --- /dev/null +++ b/src/main/resources/static/images/aqua.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/clion.svg b/src/main/resources/static/images/clion.svg new file mode 100644 index 0000000..3440b58 --- /dev/null +++ b/src/main/resources/static/images/clion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/datagrip.svg b/src/main/resources/static/images/datagrip.svg new file mode 100644 index 0000000..9cb67c5 --- /dev/null +++ b/src/main/resources/static/images/datagrip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/dataspell.svg b/src/main/resources/static/images/dataspell.svg new file mode 100644 index 0000000..59a22af --- /dev/null +++ b/src/main/resources/static/images/dataspell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/dotcover.svg b/src/main/resources/static/images/dotcover.svg new file mode 100644 index 0000000..e5c7882 --- /dev/null +++ b/src/main/resources/static/images/dotcover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/dotmemory.svg b/src/main/resources/static/images/dotmemory.svg new file mode 100644 index 0000000..b92e056 --- /dev/null +++ b/src/main/resources/static/images/dotmemory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/dottrace.svg b/src/main/resources/static/images/dottrace.svg new file mode 100644 index 0000000..436f00c --- /dev/null +++ b/src/main/resources/static/images/dottrace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/goland.svg b/src/main/resources/static/images/goland.svg new file mode 100644 index 0000000..dacd3f7 --- /dev/null +++ b/src/main/resources/static/images/goland.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/icons.svg b/src/main/resources/static/images/icons.svg new file mode 100644 index 0000000..2b7ad34 --- /dev/null +++ b/src/main/resources/static/images/icons.svg @@ -0,0 +1,713 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/images/intellij-idea.svg b/src/main/resources/static/images/intellij-idea.svg new file mode 100644 index 0000000..d6de682 --- /dev/null +++ b/src/main/resources/static/images/intellij-idea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/phpstorm.svg b/src/main/resources/static/images/phpstorm.svg new file mode 100644 index 0000000..588e24a --- /dev/null +++ b/src/main/resources/static/images/phpstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/plugin.svg b/src/main/resources/static/images/plugin.svg new file mode 100644 index 0000000..8f16040 --- /dev/null +++ b/src/main/resources/static/images/plugin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/pycharm.svg b/src/main/resources/static/images/pycharm.svg new file mode 100644 index 0000000..0956bbc --- /dev/null +++ b/src/main/resources/static/images/pycharm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/rider.svg b/src/main/resources/static/images/rider.svg new file mode 100644 index 0000000..e84f77b --- /dev/null +++ b/src/main/resources/static/images/rider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/rubymine.svg b/src/main/resources/static/images/rubymine.svg new file mode 100644 index 0000000..9343cce --- /dev/null +++ b/src/main/resources/static/images/rubymine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/rustrover.svg b/src/main/resources/static/images/rustrover.svg new file mode 100644 index 0000000..25660a6 --- /dev/null +++ b/src/main/resources/static/images/rustrover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/webstorm.svg b/src/main/resources/static/images/webstorm.svg new file mode 100644 index 0000000..4920237 --- /dev/null +++ b/src/main/resources/static/images/webstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..5ce7128 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,515 @@ + + + + + + QiuMo JetBrains Help - JetBrains激活助手 + + + + + + + + + + + + + +

+ + +
+
+
+
+ +
+

初始配置

+

请配置您的许可证信息

+
+ +
+
+ + +
+ +
+ + +
+ + +
+
+
+
+ + + + + +
+
+ +
+
+ + +
+ + +
+
+

欢迎使用 JetBrains 激活助手

+

为您提供便捷的 JetBrains 产品激活解决方案,支持激活码和许可证服务器两种激活方式

+
+ + +
+
+

当前配置

+ +
+
+
+
+ +
+
+

许可证名称

+

{{ config.licenseName || '未设置' }}

+
+
+
+
+ +
+
+

被许可人

+

{{ config.assigneeName || '未设置' }}

+
+
+
+
+ + +
+
+
+ +
+

必备代理工具

+

激活前必须先下载并配置 ja-netfilter 代理工具

+
+ +
+

配置步骤:

+
+
+ 1 +
+

下载并解压 ja-netfilter 工具包

+
+
+
+ 2 +
+

在 IDE 的 VM options 中添加以下配置:

+
+ -javaagent:插件解压路径\ja-netfilter\ja-netfilter.jar
--add-opens=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED
+ --add-opens=java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED +
+
+
+
+ 3 +
+

重启 IDE 后即可使用激活功能

+
+
+
+
+ + +
+ + +
+ +
+
+
+ +
+

激活码激活

+

生成产品激活码直接激活

+
+
+
+
+ 1 +
+

选择需要激活的产品或插件

+
+
+
+ 2 +
+

设置许可证到期时间

+
+
+
+ 3 +
+

生成激活码并在软件中输入

+
+
+
+ + +
+
+ + +
+
+
+ +
+

服务器激活

+

配置许可证服务器统一激活

+
+
+
+
+ 1 +
+

下载并配置 ja-netfilter 代理

+
+
+
+ 2 +
+

在软件中配置许可证服务器地址

+
+
+
+ 3 +
+

启动软件自动获取许可证

+
+
+
+
+

许可证服务器地址:

+
+ + +
+
+
+
+
+ + +
+
+
+ +
+
+

JRebel 专属激活

+

JRebel 仅支持服务器激活模式

+
+
+ +
+
+ + +
+
+

JetBrains 产品

+

选择需要激活的 JetBrains 产品

+ + +
+ + +
+
+ +
+
+
+
+ +
+

{{ product.name }}

+

{{ product.description || '专业的开发工具' }}

+ +
+
+
+ +
+ +

没有找到匹配的产品

+
+
+ + +
+
+

JetBrains 插件

+

选择需要激活的 JetBrains 付费插件

+ + +
+ + +
+
+ +
+
+
+
+ +
+

{{ plugin.name }}

+

{{ plugin.description || '功能强大的插件' }}

+ +
+
+
+ +
+ +

没有找到匹配的插件

+
+
+ + +
+
+

JRebel 激活

+

JRebel 仅支持服务器激活模式

+
+ +
+
+
+ +
+

JRebel 服务器激活

+

请按照以下步骤配置 JRebel 服务器激活

+
+ +
+
+
+ 1 +
+
+

复制服务器地址

+
+

JRebel 服务器地址:

+
+ + +
+
+
+
+ +
+
+ 2 +
+
+

打开 JRebel 激活界面

+

在 IDE 中找到 JRebel 插件,选择"Connect to License Server"

+
+
+ +
+
+ 3 +
+
+

输入服务器地址

+

将上述服务器地址粘贴到 JRebel 的服务器地址输入框中

+
+
+ +
+
+ 4 +
+
+

完成激活

+

点击"Activate"按钮完成激活,享受 JRebel 热部署功能

+
+
+
+ +
+
+ +
+

注意事项

+
    +
  • • 确保您的网络可以访问本服务器地址
  • +
  • • JRebel 激活后请保持服务器运行状态
  • +
  • • 如有问题,请检查防火墙和网络设置
  • +
+
+
+
+
+
+
+
+ + + +
+
+
+
+ + +
+

生成激活码

+

{{ selectedItem?.name }}

+
+ +
+
+ + +
+ +
+ + +
+
+
+
+
+ + + +
+
+
+
+ +
+

激活码生成成功

+

请复制以下激活码到软件中使用

+
+ +
+
+ 激活码: + +
+
{{ generatedLicense }}
+
+ + +
+
+
+ + +
+ +
+
+ + + + + + diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js new file mode 100644 index 0000000..20ab2ac --- /dev/null +++ b/src/main/resources/static/js/app.js @@ -0,0 +1,247 @@ +// Vue 3 主应用 +const { createApp } = Vue + +// 主应用组件 +const App = { + data() { + return { + currentPage: 'home', + showConfigModal: false, + showLicenseModal: false, + showResultModal: false, + isGenerating: false, + config: { + licenseName: '', + assigneeName: '' + }, + licenseConfig: { + expiryDate: '' + }, + selectedItem: null, + generatedLicense: '', + products: [], + plugins: [], + filteredProducts: [], + filteredPlugins: [], + searchQuery: '', + navItems: [ + { id: 'home', name: '首页', icon: 'fas fa-home' }, + { id: 'products', name: '产品', icon: 'fas fa-cube' }, + { id: 'plugins', name: '插件', icon: 'fas fa-puzzle-piece' }, + { id: 'jrebel', name: 'JRebel', icon: 'fas fa-fire' } + ], + showBackToTop: false + } + }, + + computed: { + serverUrl() { + return `${window.location.origin}` + }, + + jrebelServerUrl() { + const uuid = Utils.generateUUID() + return `${window.location.origin}/${uuid}` + } + }, + + watch: { + searchQuery(newQuery) { + this.filterItems(newQuery) + }, + + currentPage() { + this.searchQuery = '' + // 重置过滤结果 + this.filteredProducts = [...this.products] + this.filteredPlugins = [...this.plugins] + } + }, + + mounted() { + this.loadConfig() + this.loadProducts() + this.loadPlugins() + this.setDefaultExpiryDate() + + // 监听滚动事件 + const handleScroll = () => { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop + this.showBackToTop = scrollTop > 300 + } + + // 监听滚动事件 + window.addEventListener('scroll', handleScroll) + + // 保存函数引用以便清理 + this._handleScroll = handleScroll + }, + + beforeUnmount() { + // 清理滚动事件监听器 + if (this._handleScroll) { + window.removeEventListener('scroll', this._handleScroll) + document.removeEventListener('scroll', this._handleScroll) + } + }, + + methods: { + // 配置相关 + loadConfig() { + const config = StorageService.getConfig() + if (StorageService.isConfigured()) { + this.config = config + } else { + this.showConfigModal = true + } + }, + + saveConfig() { + if (this.config.licenseName && this.config.assigneeName) { + StorageService.saveConfig(this.config.licenseName, this.config.assigneeName) + this.showConfigModal = false + Utils.showNotification('配置保存成功') + } + }, + + // 数据加载 + async loadProducts() { + try { + this.products = await ApiService.getProducts() + this.filteredProducts = [...this.products] + } catch (error) { + console.error('加载产品列表失败:', error) + Utils.showNotification('加载产品列表失败', 'error') + } + }, + + async loadPlugins() { + try { + this.plugins = await ApiService.getPlugins() + this.filteredPlugins = [...this.plugins] + } catch (error) { + console.error('加载插件列表失败:', error) + Utils.showNotification('加载插件列表失败', 'error') + } + }, + + // 搜索功能 + filterItems(query) { + const searchTerm = query.toLowerCase().trim() + + if (this.currentPage === 'products') { + this.filteredProducts = this.products.filter((product) => + product.name.toLowerCase().includes(searchTerm) || + (product.description && product.description.toLowerCase().includes(searchTerm)) + ) + } else if (this.currentPage === 'plugins') { + this.filteredPlugins = this.plugins.filter((plugin) => + plugin.name.toLowerCase().includes(searchTerm) || + (plugin.description && plugin.description.toLowerCase().includes(searchTerm)) + ) + } + }, + + // 选择产品/插件 + selectProduct(product) { + this.selectedItem = product + this.showLicenseModal = true + }, + + selectPlugin(plugin) { + this.selectedItem = plugin + this.showLicenseModal = true + }, + + // 生成激活码 + async generateLicense() { + this.isGenerating = true + + try { + const isPlugin = this.selectedItem.hasOwnProperty('id') + let result + + if (isPlugin) { + result = await ApiService.generatePluginLicense(this.selectedItem.id, this.config.licenseName, this.config.assigneeName, this.licenseConfig.expiryDate) + } else { + result = await ApiService.generateLicense(this.selectedItem.productCode, this.config.licenseName, this.config.assigneeName, this.licenseConfig.expiryDate) + } + + this.generatedLicense = result + this.showLicenseModal = false + this.showResultModal = true + } catch (error) { + console.error('生成激活码失败:', error) + Utils.showNotification('生成激活码失败,请重试', 'error') + } finally { + this.isGenerating = false + } + }, + + // 工具方法 + downloadAgent() { + ApiService.downloadAgent() + }, + + copyToClipboard(text) { + Utils.copyToClipboard(text) + }, + + setDefaultExpiryDate() { + this.licenseConfig.expiryDate = Utils.getDefaultExpiryDate() + }, + + // 图标处理 + getProductIcon(product) { + if (product.iconClass && product.iconClass.startsWith('icon-')) { + const iconName = product.iconClass.replace('icon-', ''); + + // 图标映射表 + const iconMap = { + 'ii': 'https://resources.jetbrains.com/storage/logos/web/intellij-idea/intellij-idea.svg', + 'ps': 'https://resources.jetbrains.com/storage/logos/web/phpstorm/phpstorm.svg', + 'ac': 'https://resources.jetbrains.com/storage/logos/web/appcode/appcode.svg', + 'db': 'https://resources.jetbrains.com/storage/logos/web/datagrip/datagrip.svg', + 'rm': 'https://resources.jetbrains.com/storage/logos/web/rubymine/rubymine.svg', + 'ws': 'https://resources.jetbrains.com/storage/logos/web/webstorm/webstorm.svg', + 'rd': 'https://resources.jetbrains.com/storage/logos/web/rider/rider.svg', + 'cl': 'https://resources.jetbrains.com/storage/logos/web/clion/clion.svg', + 'pc': 'https://resources.jetbrains.com/storage/logos/web/pycharm/pycharm.svg', + 'go': 'https://resources.jetbrains.com/storage/logos/web/goland/goland.svg', + 'ds': 'https://resources.jetbrains.com/storage/logos/web/dataspell/dataspell.svg', + 'dc': 'https://resources.jetbrains.com/storage/logos/web/dotcover/dotcover.svg', + 'dpn': 'https://resources.jetbrains.com/storage/logos/web/dottrace/dottrace.svg', + 'dm': 'https://resources.jetbrains.com/storage/logos/web/dotmemory/dotmemory.svg', + 'rr': 'https://resources.jetbrains.com/storage/logos/web/rustrover/rustrover.svg', + 'qa': 'https://resources.jetbrains.com/storage/logos/web/aqua/aqua.svg', + 'al': 'https://resources.jetbrains.com/storage/logos/web/toolbox/toolbox.svg' + }; + + return iconMap[iconName] || '/images/plugin.svg'; + } + return '/images/plugin.svg'; + }, + + getPluginIcon(plugin) { + return plugin.icon || '/images/plugin.svg' + }, + + // 页面跳转 + navigateTo(page) { + this.currentPage = page + this.searchQuery = '' + }, + + // 返回顶部 + scrollToTop() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } + } +} + +// 启动应用 +const app = createApp(App) +app.mount('#app') diff --git a/src/main/resources/static/js/utils.js b/src/main/resources/static/js/utils.js new file mode 100644 index 0000000..b660495 --- /dev/null +++ b/src/main/resources/static/js/utils.js @@ -0,0 +1,209 @@ +// 工具函数 +const Utils = { + // 生成UUID + generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (Math.random() * 16) | 0, + v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + }, + + // 复制到剪贴板 + async copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text) + this.showNotification('已复制到剪贴板', 'success') + } catch (err) { + console.error('复制失败:', err) + // 降级方案 + const textArea = document.createElement('textarea') + textArea.value = text + document.body.appendChild(textArea) + textArea.select() + try { + document.execCommand('copy') + this.showNotification('已复制到剪贴板', 'success') + } catch (err) { + this.showNotification('复制失败,请手动复制', 'error') + } + document.body.removeChild(textArea) + } + }, + + // 显示通知 + showNotification(message, type = 'success') { + // 移除现有通知 + const existingNotification = document.querySelector('.notification') + if (existingNotification) { + existingNotification.remove() + } + + // 创建新通知 + const notification = document.createElement('div') + notification.className = `notification ${type}` + notification.innerHTML = ` +
+ + ${message} +
+ ` + + document.body.appendChild(notification) + + // 3秒后自动移除 + setTimeout(() => { + if (notification.parentNode) { + notification.remove() + } + }, 3000) + }, + + // 格式化文件大小 + formatFileSize(bytes) { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + }, + + // 设置产品图标 + setProductIcon(element, iconClass) { + if (iconClass && iconClass.startsWith('icon-')) { + const iconName = iconClass.replace('icon-', '') + element.style.backgroundImage = `url('/images/${iconName}.svg')` + element.className = 'product-icon mx-auto mb-4 rounded-2xl' + } + }, + + // 防抖函数 + debounce(func, wait) { + let timeout + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout) + func(...args) + } + clearTimeout(timeout) + timeout = setTimeout(later, wait) + } + }, + + // 格式化日期 + formatDate(date) { + return new Date(date).toLocaleDateString('zh-CN') + }, + + // 验证日期格式 + isValidDate(dateString) { + const regex = /^\d{4}-\d{2}-\d{2}$/ + if (!regex.test(dateString)) return false + const date = new Date(dateString) + return date instanceof Date && !isNaN(date) + }, + + // 获取默认过期日期(一年后) + getDefaultExpiryDate() { + const date = new Date() + date.setFullYear(date.getFullYear() + 1) + return date.toISOString().split('T')[0] + } +} + +// API 服务 +const ApiService = { + baseURL: window.location.origin, + + async get(url) { + try { + const response = await fetch(`${this.baseURL}${url}`) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return await response.json() + } catch (error) { + console.error('API GET error:', error) + throw error + } + }, + + async getText(url) { + try { + const response = await fetch(`${this.baseURL}${url}`) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return await response.text() + } catch (error) { + console.error('API GET text error:', error) + throw error + } + }, + + // 获取产品列表 + async getProducts() { + return await this.get('/api/products') + }, + + // 获取插件列表 + async getPlugins() { + return await this.get('/api/plugins') + }, + + // 生成产品激活码 + async generateLicense(productCode, licenseeName, assigneeName, expiryDate) { + const params = new URLSearchParams({ + ...(productCode && { productCode }), + licenseeName, + assigneeName, + expiryDate + }) + return await this.getText(`/license-code/generate?${params}`) + }, + + // 生成插件激活码 + async generatePluginLicense(pluginId, licenseeName, assigneeName, expiryDate) { + const params = new URLSearchParams({ + pluginId, + licenseeName, + assigneeName, + expiryDate + }) + return await this.getText(`/license-code/generate-plugin?${params}`) + }, + + // 下载代理工具 + downloadAgent() { + window.open('/ja-netfilter', '_blank') + } +} + +// 存储服务 +const StorageService = { + // 保存配置 + saveConfig(licenseName, assigneeName) { + localStorage.setItem('licenseName', licenseName) + localStorage.setItem('assigneeName', assigneeName) + }, + + // 获取配置 + getConfig() { + return { + licenseName: localStorage.getItem('licenseName') || '', + assigneeName: localStorage.getItem('assigneeName') || '' + } + }, + + // 检查是否已配置 + isConfigured() { + const config = this.getConfig() + return config.licenseName && config.assigneeName + }, + + // 清除配置 + clearConfig() { + localStorage.removeItem('licenseName') + localStorage.removeItem('assigneeName') + } +} diff --git a/src/test/java/com/qiumo/help/QiuMoJetbrainsHelpApplicationTests.java b/src/test/java/com/qiumo/help/QiuMoJetbrainsHelpApplicationTests.java new file mode 100644 index 0000000..1b8236a --- /dev/null +++ b/src/test/java/com/qiumo/help/QiuMoJetbrainsHelpApplicationTests.java @@ -0,0 +1,13 @@ +package com.qiumo.help; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class QiuMoJetbrainsHelpApplicationTests { + + @Test + void contextLoads() { + } + +}