navicat链接导入

This commit is contained in:
lzy
2023-09-18 09:53:34 +08:00
parent 18e9cf7489
commit c1f04ca0ff
10 changed files with 676 additions and 0 deletions

View File

@ -0,0 +1,65 @@
package ai.chat2db.server.web.api.controller.ncx;
import ai.chat2db.server.tools.base.wrapper.result.DataResult;
import ai.chat2db.server.tools.common.util.ConfigUtils;
import ai.chat2db.server.web.api.aspect.ConnectionInfoAspect;
import ai.chat2db.server.web.api.controller.ncx.service.ConverterService;
import ai.chat2db.server.web.api.controller.ncx.vo.UploadVO;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Objects;
/**
* ConverterController
*
* @author lzy
**/
@RequestMapping("/api/converter")
@RestController
@Slf4j
public class ConverterController {
@Autowired
private ConverterService converterService;
@SneakyThrows
@PostMapping("/ncx/upload")
public DataResult<UploadVO> uploadFile(@RequestParam("file") MultipartFile file) {
// 验证文件后缀
String fileExtension = getFileExtension(Objects.requireNonNull(file.getOriginalFilename()));
if (!isAllowedExtension(fileExtension)) {
return DataResult.error("1", "上传的文件必须是ncx文件");
}
File temp = new File(ConfigUtils.CONFIG_BASE_PATH + File.separator + "temp.tmp");
file.transferTo(temp);
return DataResult.of(converterService.uploadFile(temp));
}
private String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex > 0) {
return fileName.substring(dotIndex + 1).toLowerCase();
} else {
return "";
}
}
private boolean isAllowedExtension(String extension) {
// 只允许上传的文件后缀
String[] allowedExtensions = {"ncx"};
for (String ext : allowedExtensions) {
if (ext.equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,44 @@
package ai.chat2db.server.web.api.controller.ncx.cipher;
import java.util.Formatter;
/**
* CommonCipher 公共加/解密
*
* @author lzy
*/
public abstract class CommonCipher {
public String encryptString(String plaintext) {
return null;
}
public String decryptString(String ciphertext) {
return null;
}
public String printHexBinary(byte[] data) {
StringBuilder hexBuilder = new StringBuilder();
Formatter formatter = new Formatter(hexBuilder);
for (byte b : data) {
formatter.format("%02x", b);
}
return hexBuilder.toString();
}
public static byte[] parseHexBinary(String data) {
return hexStringToByteArray(data);
}
public static byte[] hexStringToByteArray(String hex) {
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException("Hex string length must be even");
}
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
String byteString = hex.substring(i, i + 2);
bytes[i / 2] = (byte) Integer.parseInt(byteString, 16);
}
return bytes;
}
}

View File

@ -0,0 +1,177 @@
package ai.chat2db.server.web.api.controller.ncx.cipher;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* Navicat11及以下密码加密解密
*
* @author lzy
*/
public class Navicat11Cipher extends CommonCipher {
public static final String DefaultUserKey = "3DC5CA39";
private static byte[] IV;
private static SecretKeySpec key;
private static Cipher encryptor;
private static Cipher decrypt;
private static void initKey() {
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
byte[] userKey_data = Navicat11Cipher.DefaultUserKey.getBytes(StandardCharsets.UTF_8);
sha1.update(userKey_data, 0, userKey_data.length);
key = new SecretKeySpec(sha1.digest(), "Blowfish");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void initCipherEncrypt() {
try {
// Must use NoPadding
encryptor = Cipher.getInstance("Blowfish/ECB/NoPadding");
encryptor.init(Cipher.ENCRYPT_MODE, key);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void initCipherDecrypt() {
try {
// Must use NoPadding
decrypt = Cipher.getInstance("Blowfish/ECB/NoPadding");
decrypt.init(Cipher.DECRYPT_MODE, key);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void initIV() {
try {
byte[] initVec = parseHexBinary("FFFFFFFFFFFFFFFF");
IV = encryptor.doFinal(initVec);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void xorBytes(byte[] a, byte[] b) {
for (int i = 0; i < a.length; i++) {
int aVal = a[i] & 0xff; // convert byte to integer
int bVal = b[i] & 0xff;
a[i] = (byte) (aVal ^ bVal); // xor aVal and bVal and typecast to byte
}
}
private void xorBytes(byte[] a, byte[] b, int l) {
for (int i = 0; i < l; i++) {
int aVal = a[i] & 0xff; // convert byte to integer
int bVal = b[i] & 0xff;
a[i] = (byte) (aVal ^ bVal); // xor aVal and bVal and typecast to byte
}
}
static {
initKey();
initCipherEncrypt();
initCipherDecrypt();
initIV();
}
private byte[] Encrypt(byte[] inData) {
try {
byte[] CV = Arrays.copyOf(IV, IV.length);
byte[] ret = new byte[inData.length];
int blocks_len = inData.length / 8;
int left_len = inData.length % 8;
for (int i = 0; i < blocks_len; i++) {
byte[] temp = Arrays.copyOfRange(inData, i * 8, (i * 8) + 8);
xorBytes(temp, CV);
temp = encryptor.doFinal(temp);
xorBytes(CV, temp);
System.arraycopy(temp, 0, ret, i * 8, 8);
}
if (left_len != 0) {
CV = encryptor.doFinal(CV);
byte[] temp = Arrays.copyOfRange(inData, blocks_len * 8, (blocks_len * 8) + left_len);
xorBytes(temp, CV, left_len);
System.arraycopy(temp, 0, ret, blocks_len * 8, temp.length);
}
return ret;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public String encryptString(String inputString) {
try {
byte[] inData = inputString.getBytes(StandardCharsets.UTF_8);
byte[] outData = Encrypt(inData);
return printHexBinary(outData);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private byte[] Decrypt(byte[] inData) {
try {
byte[] cv = Arrays.copyOf(IV, IV.length);
byte[] ret = new byte[inData.length];
int blocks_len = inData.length / 8;
int left_len = inData.length % 8;
for (int i = 0; i < blocks_len; i++) {
byte[] temp = Arrays.copyOfRange(inData, i * 8, (i * 8) + 8);
temp = decrypt.doFinal(temp);
xorBytes(temp, cv);
System.arraycopy(temp, 0, ret, i * 8, 8);
for (int j = 0; j < cv.length; j++) {
cv[j] = (byte) (cv[j] ^ inData[i * 8 + j]);
}
}
if (left_len != 0) {
cv = encryptor.doFinal(cv);
byte[] temp = Arrays.copyOfRange(inData, blocks_len * 8, (blocks_len * 8) + left_len);
xorBytes(temp, cv, left_len);
for (int j = 0; j < temp.length; j++) {
ret[blocks_len * 8 + j] = temp[j];
}
}
return ret;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public String decryptString(String hexString) {
if (StringUtils.isEmpty(hexString)) {
return "";
}
try {
byte[] inData = parseHexBinary(hexString);
byte[] outData = Decrypt(inData);
return new String(outData, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,50 @@
package ai.chat2db.server.web.api.controller.ncx.cipher;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* Navicat12及以上密码加密解密
*
* @author lzy
*/
public class Navicat12Cipher extends CommonCipher {
private static final SecretKeySpec AES_KEY;
private static final IvParameterSpec AES_IV;
static {
AES_KEY = new SecretKeySpec("libcckeylibcckey".getBytes(StandardCharsets.UTF_8), "AES");
AES_IV = new IvParameterSpec("libcciv libcciv ".getBytes(StandardCharsets.UTF_8));
}
@Override
public String encryptString(String plaintext) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, AES_KEY, AES_IV);
byte[] ret = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return printHexBinary(ret);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public String decryptString(String ciphertext) {
if (StringUtils.isEmpty(ciphertext)) {
return "";
}
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, AES_KEY, AES_IV);
byte[] ret = cipher.doFinal(parseHexBinary(ciphertext));
return new String(ret, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,87 @@
package ai.chat2db.server.web.api.controller.ncx.enums;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/**
* DataBaseType
*
* @author lzy
**/
@Getter
public enum DataBaseType {
/**
* MYSQL
*/
MYSQL("jdbc:mysql://%s:%s"),
/**
* ORACLE
*/
ORACLE("jdbc:oracle:thin:@%s:%s:XE"),
/**
* SQL_SERVER
*/
SQLSERVER("jdbc:sqlserver://%s:%s"),
/**
* SQL_SERVER
*/
SQLITE("jdbc:sqlite:%s"),
/**
* POSTGRESQL
**/
POSTGRESQL("jdbc:postgresql://%s:%s"),
/**
* DB2
**/
DB2("jdbc:db2://%s:%s"),
/**
* Mariadb
**/
Mariadb("jdbc:mariadb://%s:%s"),
/**
* DM
**/
DM("jdbc:dm://%s:%s"),
/**
* KINGBASE8
**/
KINGBASE8("jdbc:kingbase8://%s:%s"),
/**
* Presto
**/
Presto("jdbc:presto://%s:%s"),
/**
* OceanBase
**/
OceanBase("jdbc:oceanbase://%s:%s"),
/**
* Hive
**/
Hive("jdbc:hive2://%s:%s"),
/**
* ClickHouse
**/
ClickHouse("jdbc:clickhouse://%s:%s");
private String urlString;
DataBaseType(String urlString) {
this.urlString = urlString;
}
public void setUrlString(String urlString) {
this.urlString = urlString;
}
public static DataBaseType matchType(String value) {
if (StringUtils.isNotEmpty(value)) {
for (DataBaseType dataBase : DataBaseType.values()) {
if (dataBase.name().equals(value.toUpperCase())) {
return dataBase;
}
}
}
return null;
}
}

View File

@ -0,0 +1,17 @@
package ai.chat2db.server.web.api.controller.ncx.enums;
/**
* navicat版本枚举版本区分navicat加密算法
*
* @author lzy
*/
public enum VersionEnum {
/**
* navicat11
*/
native11,
/**
* navicat12+
*/
navicat12more
}

View File

@ -0,0 +1,45 @@
package ai.chat2db.server.web.api.controller.ncx.factory;
import ai.chat2db.server.web.api.controller.ncx.cipher.CommonCipher;
import ai.chat2db.server.web.api.controller.ncx.cipher.Navicat11Cipher;
import ai.chat2db.server.web.api.controller.ncx.cipher.Navicat12Cipher;
import ai.chat2db.server.web.api.controller.ncx.enums.VersionEnum;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* CipherFactory
*
* @author lzy
**/
@Service
public class CipherFactory {
/**
* NavicatCipher缓存池
*/
private static final Map<String, CommonCipher> REPORT_POOL = new ConcurrentHashMap<>(0);
static {
REPORT_POOL.put(VersionEnum.native11.name(), new Navicat11Cipher());
REPORT_POOL.put(VersionEnum.navicat12more.name(), new Navicat12Cipher());
}
/**
* 获取对应加/解密方法
*
* @param type 类型
* @return ITokenGranter
*/
@SneakyThrows
public static CommonCipher get(String type) {
CommonCipher cipher = REPORT_POOL.get(type);
if (cipher == null) {
throw new ClassNotFoundException("no CommonCipher was found");
} else {
return cipher;
}
}
}

View File

@ -0,0 +1,16 @@
package ai.chat2db.server.web.api.controller.ncx.service;
import ai.chat2db.server.web.api.controller.ncx.vo.UploadVO;
import java.io.File;
import java.io.InputStream;
/**
* ConverterService
*
* @author lzy
**/
public interface ConverterService {
UploadVO uploadFile(File file);
}

View File

@ -0,0 +1,156 @@
package ai.chat2db.server.web.api.controller.ncx.service.impl;
import ai.chat2db.server.domain.core.util.DesUtil;
import ai.chat2db.server.domain.repository.entity.DataSourceDO;
import ai.chat2db.server.domain.repository.mapper.DataSourceMapper;
import ai.chat2db.server.web.api.controller.ncx.cipher.CommonCipher;
import ai.chat2db.server.web.api.controller.ncx.enums.DataBaseType;
import ai.chat2db.server.web.api.controller.ncx.enums.VersionEnum;
import ai.chat2db.server.web.api.controller.ncx.factory.CipherFactory;
import ai.chat2db.server.web.api.controller.ncx.service.ConverterService;
import ai.chat2db.server.web.api.controller.ncx.vo.UploadVO;
import ai.chat2db.spi.model.SSHInfo;
import com.alibaba.excel.util.FileUtils;
import com.alibaba.fastjson2.JSON;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* ConverterServiceImpl
*
* @author lzy
**/
@Service
@Transactional(rollbackFor = Exception.class)
public class ConverterServiceImpl implements ConverterService {
private static final double NAVICAT11 = 1.1D;
private static CommonCipher cipher;
@Autowired
private DataSourceMapper dataSourceMapper;
@Override
public UploadVO uploadFile(File file) {
UploadVO vo = new UploadVO();
try {
// List<Map <连接名Map<属性名,值>>> 要导入的连接
List<Map<String, Map<String, String>>> configMap = new ArrayList<>();
//1、创建一个DocumentBuilderFactory的对象
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//2、创建一个DocumentBuilder的对象
//创建DocumentBuilder对象
DocumentBuilder db = dbf.newDocumentBuilder();
//3、通过DocumentBuilder对象的parser方法加载xml文件到当前项目下
Document document = db.parse(file);
//获取所有Connections节点的集合
NodeList connectList = document.getElementsByTagName("Connection");
NodeList nodeList = document.getElementsByTagName("Connections");
//选中第一个节点
NamedNodeMap verMap = nodeList.item(0).getAttributes();
double version = Double.parseDouble((verMap.getNamedItem("Ver").getNodeValue()));
if (version <= NAVICAT11) {
cipher = CipherFactory.get(VersionEnum.native11.name());
} else {
cipher = CipherFactory.get(VersionEnum.navicat12more.name());
}
//配置map
Map<String, Map<String, String>> connectionMap = new HashMap<>();
//遍历每一个Connections节点
for (int i = 0; i < connectList.getLength(); i++) {
//通过 item(i)方法 获取一个Connection节点nodeList的索引值从0开始
Node connect = connectList.item(i);
//获取Connection节点的所有属性集合
NamedNodeMap attrs = connect.getAttributes();
//遍历Connection的属性
Map<String, String> map = new HashMap<>(0);
for (int j = 0; j < attrs.getLength(); j++) {
//通过item(index)方法获取connect节点的某一个属性
Node attr = attrs.item(j);
map.put(attr.getNodeName(), attr.getNodeValue());
}
connectionMap.put(map.get("ConnectionName") + map.get("ConnType"), map);
}
configMap.add(connectionMap);
// 将获取到navicat导入的链接写入chat2db的h2数据库
insertDBConfig(configMap);
//删除临时文件
FileUtils.delete(file);
} catch (Exception e) {
throw new RuntimeException(e);
}
return vo;
}
/**
* 写入到数据库
*
* @param list 读取ncx文件的数据
*/
@SneakyThrows
public void insertDBConfig(List<Map<String, Map<String, String>>> list) {
for (Map<String, Map<String, String>> map : list) {
for (Map.Entry<String, Map<String, String>> valueMap : map.entrySet()) {
Map<String, String> resultMap = valueMap.getValue();
// 解密密码
String password = cipher.decryptString(resultMap.getOrDefault("Password", ""));
DataSourceDO dataSourceDO = new DataSourceDO();
LocalDateTime dateTime = LocalDateTime.now();
dataSourceDO.setGmtCreate(dateTime);
dataSourceDO.setGmtModified(dateTime);
dataSourceDO.setAlias(resultMap.get("ConnectionName"));
dataSourceDO.setHost(resultMap.get("Host"));
dataSourceDO.setPort(resultMap.get("Port"));
dataSourceDO.setUserName(resultMap.get("UserName"));
dataSourceDO.setType(resultMap.get("ConnType"));
// mysql的版本还无法区分
dataSourceDO.setUrl(String.format(Objects.requireNonNull(DataBaseType.matchType(dataSourceDO.getType())).getUrlString(), dataSourceDO.getHost(), dataSourceDO.getPort()));
//password 为解密出来的密文再使用chat2db的加密
DesUtil desUtil = new DesUtil(DesUtil.DES_KEY);
String encryptStr = desUtil.encrypt(password, "CBC");
dataSourceDO.setPassword(encryptStr);
SSHInfo sshInfo = new SSHInfo();
if ("false".equals(resultMap.get("SSH"))) {
sshInfo.setUse(false);
} else {
sshInfo.setUse(true);
sshInfo.setHostName(resultMap.get("SSH_Host"));
sshInfo.setPort(resultMap.get("SSH_Port"));
sshInfo.setUserName(resultMap.get("SSH_UserName"));
// 目前chat2DB只支持 password 和 Private key
boolean passwordType = "password".equalsIgnoreCase(resultMap.get("SSH_AuthenMethod"));
sshInfo.setAuthenticationType(passwordType ? "password" : "Private key");
if (passwordType) {
// 解密密码
String ssh_password = cipher.decryptString(resultMap.getOrDefault("SSH_Password", ""));
sshInfo.setPassword(ssh_password);
} else {
sshInfo.setKeyFile(resultMap.get("SSH_PrivateKey"));
sshInfo.setPassphrase(resultMap.get("SSH_Passphrase"));
}
}
dataSourceDO.setSsh(JSON.toJSONString(sshInfo));
dataSourceMapper.insert(dataSourceDO);
}
}
}
}

View File

@ -0,0 +1,19 @@
package ai.chat2db.server.web.api.controller.ncx.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* UploadVO
*
* @author lzy
**/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class UploadVO {
private String result;
}