commit cd4f13190f09108bec774484d6a217f67db71c40 Author: M0914 <2625053283@qq.com> Date: Thu Sep 11 14:19:43 2025 +0800 enjoy!!! 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 0000000..6f715c6 Binary files /dev/null and b/src/main/resources/external/agent/ja-netfilter.zip differ 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() { + } + +}