Skip to content

Commit

Permalink
feat: advance function cooperate
Browse files Browse the repository at this point in the history
  • Loading branch information
zyyzyykk committed Feb 2, 2025
1 parent a98177d commit fed7ded
Show file tree
Hide file tree
Showing 27 changed files with 574 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

public enum ResultCodeEnum {

COOPERATE_KEY_INVALID(-2, "Cooperate Key is invalid"),

CONNECT_FAIL(-1,"Fail to connect remote server"),

CONNECT_SUCCESS(0,"Connecting success"),

OUT_TEXT(1,"Text output to terminal");
OUT_TEXT(1,"Text output to terminal"),

COOPERATE_NUMBER_UPDATE(2,"Update cooperate number");

private Integer state;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
import com.kkbpro.terminal.constants.enums.CharsetEnum;
import com.kkbpro.terminal.constants.enums.MessageInfoTypeRnum;
import com.kkbpro.terminal.constants.enums.ResultCodeEnum;
import com.kkbpro.terminal.controller.AdvanceController;
import com.kkbpro.terminal.pojo.dto.CooperateInfo;
import com.kkbpro.terminal.pojo.dto.EnvInfo;
import com.kkbpro.terminal.pojo.dto.MessageInfo;
import com.kkbpro.terminal.pojo.dto.PrivateKey;
import com.kkbpro.terminal.result.Result;
import com.kkbpro.terminal.utils.AesUtil;
import com.kkbpro.terminal.utils.FileUtil;
import com.kkbpro.terminal.utils.StringUtil;
import lombok.Getter;
import lombok.Setter;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.sftp.SFTPClient;
Expand All @@ -31,25 +35,35 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/socket/ssh/{env}") // 注意不要以'/'结尾
public class WebSocketServer {

public static ConcurrentHashMap<String, Session> webSessionMap = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, WebSocketServer> webSocketServerMap = new ConcurrentHashMap<>();

public static ConcurrentHashMap<String, SSHClient> sshClientMap = new ConcurrentHashMap<>();

public static ConcurrentHashMap<String, ConcurrentHashMap<String, String>> fileUploadingMap = new ConcurrentHashMap<>();

public static ConcurrentHashMap<String, SFTPClient> sftpClientMap = new ConcurrentHashMap<>();

public static ConcurrentHashMap<String, List<Session>> cooperateMap = new ConcurrentHashMap<>();

private static AppConfig appConfig;

@Getter
private Session sessionSocket = null;

@Getter @Setter
private CooperateInfo cooperateInfo;

private Boolean cooperator = false;

private String sshKey = null;

private SSHClient sshClient;
Expand Down Expand Up @@ -80,6 +94,45 @@ public void onOpen(Session sessionSocket, @PathParam("env") String env) throws I
// 设置最大空闲超时(上线后失效???)
sessionSocket.setMaxIdleTimeout(appConfig.getMaxIdleTimeout());

// 协作
String cooperateKey = envInfo.getCooperateKey();
if(cooperateKey != null && !cooperateKey.isEmpty()) {
Integer state = ResultCodeEnum.COOPERATE_KEY_INVALID.getState();
String msg = ResultCodeEnum.COOPERATE_KEY_INVALID.getDesc();
try {
String sshKey = AesUtil.aesDecrypt(StringUtil.changeStrBase64(cooperateKey), AdvanceController.COOPERATE_SECRET_KEY);
WebSocketServer webSocketServer = WebSocketServer.webSocketServerMap.get(sshKey);
if(webSocketServer == null || webSocketServer.cooperateInfo == null)
throw new RuntimeException();

List<Session> sessions = WebSocketServer.cooperateMap.computeIfAbsent(sshKey, k -> new ArrayList<>());
synchronized (sessions) {
Integer maxHeadCount = webSocketServer.cooperateInfo.getMaxHeadCount();
Boolean readOnly = webSocketServer.cooperateInfo.getReadOnly();
// 成功加入协作
if(maxHeadCount > sessions.size()) {
state = ResultCodeEnum.CONNECT_SUCCESS.getState();
msg = (readOnly ? "ReadOnly" : "Edit") + " cooperate connect success";
sessions.add(sessionSocket);
this.sshKey = sshKey;
this.cooperator = true;
this.serverCharset = webSocketServer.serverCharset;
if(!readOnly) {
this.shell = webSocketServer.shell;
this.shellOutputStream = webSocketServer.shellOutputStream;
}
sendMessage(webSocketServer.sessionSocket, ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getDesc(),
"success", ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getState(), Integer.toString(sessions.size()));
}
else msg = "Cooperators limit exceeded";
}
} catch (Exception e) {
e.printStackTrace();
}
sendMessage(sessionSocket, msg, "fail", state, null);
return;
}

// 与服务器建立连接
String host = envInfo.getServer_ip();
int port = envInfo.getServer_port();
Expand All @@ -93,9 +146,9 @@ public void onOpen(Session sessionSocket, @PathParam("env") String env) throws I

try {
sshClient.setConnectTimeout(appConfig.getSshMaxTimeout());
sshClient.addHostKeyVerifier(new PromiscuousVerifier()); // 不验证主机密钥
sshClient.addHostKeyVerifier(new PromiscuousVerifier()); // 不验证主机密钥
sshClient.connect(host,port);
if(authType != 1) sshClient.authPassword(user_name, password); // 使用用户名和密码进行身份验证
if(authType != 1) sshClient.authPassword(user_name, password); // 使用用户名和密码进行身份验证
else {
// 创建本地私钥文件
String keyPath = FileUtil.folderBasePath + "/keyProviders/" + UUID.randomUUID();
Expand Down Expand Up @@ -136,7 +189,7 @@ public void onOpen(Session sessionSocket, @PathParam("env") String env) throws I
// 连接成功,生成key标识
sshKey = envInfo.getLang() + "-" + serverCharset.name().replace("-","@") + "-" + UUID.randomUUID();
sendMessage(sessionSocket, "SSHKey","success", ResultCodeEnum.CONNECT_SUCCESS.getState(), sshKey);
webSessionMap.put(sshKey, sessionSocket);
webSocketServerMap.put(sshKey, this);
sshClientMap.put(sshKey, sshClient);
fileUploadingMap.put(sshKey, new ConcurrentHashMap<>());
// 欢迎语
Expand Down Expand Up @@ -164,6 +217,13 @@ public void onOpen(Session sessionSocket, @PathParam("env") String env) throws I
String shellOut = new String(buffer, 0, len, serverCharset);
sendMessage(sessionSocket, "ShellOut",
"success", ResultCodeEnum.OUT_TEXT.getState(), shellOut);
List<Session> sessions = cooperateMap.get(sshKey);
if(sessions != null) {
for (Session session : sessions) {
sendMessage(session, "ShellOut",
"success", ResultCodeEnum.OUT_TEXT.getState(), shellOut);
}
}
}
} catch (IOException e) {
e.printStackTrace();
Expand All @@ -174,6 +234,18 @@ public void onOpen(Session sessionSocket, @PathParam("env") String env) throws I

@OnClose
public void onClose() throws IOException {
if(cooperator) {
List<Session> sessions = cooperateMap.get(sshKey);
if(sessions != null) {
sessions.remove(this.sessionSocket);
WebSocketServer webSocketServer = webSocketServerMap.get(sshKey);
if(webSocketServer != null) {
sendMessage(webSocketServer.sessionSocket, ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getDesc(),
"success", ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getState(), Integer.toString(sessions.size()));
}
}
return;
}
// 删除临时文件
String key = sshKey;
Thread deleteTmpFileThread = new Thread(() -> {
Expand Down Expand Up @@ -206,9 +278,10 @@ public void onClose() throws IOException {
shellInputStream.close();
if(shell != null)
shell.close();
if(webSessionMap.get(key) != null)
webSessionMap.get(key).close();
webSessionMap.remove(key);
if(webSocketServerMap.get(key) != null && webSocketServerMap.get(key).sessionSocket != null)
webSocketServerMap.get(key).sessionSocket.close();
webSocketServerMap.remove(key);
cooperateMap.remove(key);
sessionSocket = null;
if(fileUploadingMap.get(key) == null || fileUploadingMap.get(key).isEmpty()) {
fileUploadingMap.remove(key);
Expand All @@ -226,6 +299,7 @@ public void onClose() throws IOException {
// 从Client接收消息
@OnMessage
public void onMessage(String message) throws IOException {
if(shell == null || shellOutputStream == null) return;

message = AesUtil.aesDecrypt(message);
MessageInfo messageInfo = JSONObject.parseObject(message, MessageInfo.class);
Expand All @@ -244,6 +318,12 @@ public void onMessage(String message) throws IOException {
if(MessageInfoTypeRnum.HEART_BEAT.getState().equals(messageInfo.getType())) {
shellOutputStream.write("".getBytes(StandardCharsets.UTF_8));
shellOutputStream.flush();
// 更新协作者数量
List<Session> sessions = cooperateMap.get(sshKey);
if(sessions != null) {
sendMessage(sessionSocket, ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getDesc(),
"success", ResultCodeEnum.COOPERATE_NUMBER_UPDATE.getState(), Integer.toString(sessions.size()));
}
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.kkbpro.terminal.controller;

import com.kkbpro.terminal.constants.enums.FileBlockStateEnum;
import com.kkbpro.terminal.consumer.WebSocketServer;
import com.kkbpro.terminal.pojo.dto.CooperateInfo;
import com.kkbpro.terminal.result.Result;
import com.kkbpro.terminal.utils.AesUtil;
import com.kkbpro.terminal.utils.StringUtil;
import net.schmizz.sshj.SSHClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 高级功能接口类
*/
@RestController
@RequestMapping("/api")
public class AdvanceController {

public static final String COOPERATE_SECRET_KEY = "o4D1fYuVp2js9xKX";

/**
* 获取协作id
*/
@GetMapping("/cooperate")
public Result getCooperateId(String sshKey, Boolean readOnly, Integer maxHeadCount) throws Exception {
String errorMsg = "协作Key生成失败";
String successMsg = "协作Key生成成功";

SSHClient ssh = WebSocketServer.sshClientMap.get(sshKey);
WebSocketServer webSocketServer = WebSocketServer.webSocketServerMap.get(sshKey);
if(ssh == null || webSocketServer == null) {
return Result.error(FileBlockStateEnum.SSH_NOT_EXIST.getState(),"连接断开," + errorMsg);
}
if(webSocketServer.getCooperateInfo() != null)
return Result.error(errorMsg);

CooperateInfo cooperateInfo = new CooperateInfo();
cooperateInfo.setReadOnly(readOnly);
cooperateInfo.setMaxHeadCount(maxHeadCount);
webSocketServer.setCooperateInfo(cooperateInfo);

String key = StringUtil.changeBase64Str(AesUtil.aesEncrypt(sshKey, COOPERATE_SECRET_KEY));

return Result.success(successMsg, key);
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,8 @@ private SFTPClient getSftpClient(String sshKey) throws IOException {
}

private void sshClose(String sshKey) {
if(WebSocketServer.webSessionMap.get(sshKey) == null && (WebSocketServer.fileUploadingMap.get(sshKey) == null || WebSocketServer.fileUploadingMap.get(sshKey).isEmpty())) {
if((WebSocketServer.webSocketServerMap.get(sshKey) == null || WebSocketServer.webSocketServerMap.get(sshKey).getSessionSocket() == null)
&& (WebSocketServer.fileUploadingMap.get(sshKey) == null || WebSocketServer.fileUploadingMap.get(sshKey).isEmpty())) {
try {
WebSocketServer.fileUploadingMap.remove(sshKey);
if(WebSocketServer.sftpClientMap.get(sshKey) != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.kkbpro.terminal.pojo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 协作相关
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CooperateInfo {

private Boolean readOnly;

private Integer maxHeadCount;

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public class EnvInfo {

private String lang = "en";

private String cooperateKey = null;

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ public static String changeStr(String str) {
return result.toString();
}

// 将Base64编码转换为合法的URL参数
public static String changeBase64Str(String str) {
return str.replace("+", "-").replace("=", "@");
}

public static String changeStrBase64(String str) {
return str.replace("-", "+").replace("@", "=");
}

}
2 changes: 1 addition & 1 deletion backend/terminal/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=100MB

# PC端启用窗口
kk.pc.window=true
kk.pc.window=false
4 changes: 3 additions & 1 deletion backend/terminal/src/main/resources/locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@
"文件内容": "File Content",
"文件不存在": "No Such File",
"云端文件过多": "Too many Cloud Files",
"云端上传成功": "Cloud File Uploaded"
"云端上传成功": "Cloud File Uploaded",
"协作Key生成成功": "Cooperate Key Generate Success",
"协作Key生成失败": "Cooperate Key Generate Failed"
}
18 changes: 9 additions & 9 deletions front/terminal/src/components/ConnectSetting.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<el-dialog
v-model="DialogVisilble"
v-model="DialogVisible"
:destory-on-close="true"
:before-close="closeDialog"
:width="360"
Expand Down Expand Up @@ -139,7 +139,7 @@ export default {
setup(props,context) {
// 控制Dialog显示
const DialogVisilble = ref(false);
const DialogVisible = ref(false);
const err_msg = ref('');
const isNewWindow = ref(false);
Expand Down Expand Up @@ -196,7 +196,7 @@ export default {
}
optionBlockType.value = type;
optionBlockRef.value.aimOption = '';
optionBlockRef.value.DialogVisilble = true;
optionBlockRef.value.DialogVisible = true;
};
const isForbidInput = ref(false);
Expand Down Expand Up @@ -238,7 +238,7 @@ export default {
const openprivateKeyBlock = () => {
privateKeyRef.value.content = setInfo.value.server_key ? setInfo.value.server_key.content : '';
privateKeyRef.value.passphrase = setInfo.value.server_key ? setInfo.value.server_key.passphrase : '';
privateKeyRef.value.DialogVisilble = true;
privateKeyRef.value.DialogVisible = true;
};
const savePrivateKey = (content,passphrase) => {
setInfo.value.server_key = {
Expand Down Expand Up @@ -299,23 +299,23 @@ export default {
};
if(setInfo.value.option != '') isForbidInput.value = true;
else isForbidInput.value = false;
DialogVisilble.value = false;
DialogVisible.value = false;
};
// 关闭
const closeDialog = (done) => {
if(optionBlockRef.value && optionBlockRef.value.DialogVisilble) optionBlockRef.value.closeDialog();
if(privateKeyRef.value && privateKeyRef.value.DialogVisilble) privateKeyRef.value.closeDialog();
if(optionBlockRef.value && optionBlockRef.value.DialogVisible) optionBlockRef.value.closeDialog();
if(privateKeyRef.value && privateKeyRef.value.DialogVisible) privateKeyRef.value.closeDialog();
setTimeout(() => {
reset();
},400);
DialogVisilble.value = false;
DialogVisible.value = false;
if(done) done();
};
return {
setInfo,
DialogVisilble,
DialogVisible,
err_msg,
isNewWindow,
confirm,
Expand Down
Loading

0 comments on commit fed7ded

Please sign in to comment.