2024. 11. 19. 15:14ㆍ카테고리 없음
리눅스 명령어를 실행해주는 Spring Boot 에이전트를 개발하려면 다음과 같은 단계를 따라가면 됩니다.
1. 프로젝트 초기 설정
1. Spring Boot 프로젝트를 생성하세요.
• 필요한 의존성:
• Spring Web
• Spring Boot Actuator (옵션)
• Spring Security (옵션: 보안 필요 시)
2. Gradle 또는 Maven으로 의존성을 추가합니다.
2. 커맨드 실행 로직 구현
Java에서는 ProcessBuilder 또는 Runtime.getRuntime()를 사용하여 명령어를 실행할 수 있습니다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandExecutor {
public String executeCommand(String command) throws Exception {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", command);
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed with exit code " + exitCode);
}
return output.toString();
}
}
3. REST API 개발
Spring Boot를 사용해 REST API로 요청을 받을 수 있도록 컨트롤러를 구현합니다.
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/command")
public class CommandController {
private final CommandExecutor commandExecutor;
public CommandController() {
this.commandExecutor = new CommandExecutor();
}
@PostMapping("/execute")
public String executeCommand(@RequestBody CommandRequest request) {
try {
return commandExecutor.executeCommand(request.getCommand());
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
}
class CommandRequest {
private String command;
// Getter and Setter
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
}
4. 보안 및 검증
(1) 명령어 제한
실행 가능한 명령어를 화이트리스트로 제한합니다.
import java.util.List;
public class CommandValidator {
private static final List<String> ALLOWED_COMMANDS = List.of("ls", "pwd", "whoami");
public static boolean isValidCommand(String command) {
return ALLOWED_COMMANDS.stream().anyMatch(command::startsWith);
}
}
CommandController에서 사용:
if (!CommandValidator.isValidCommand(request.getCommand())) {
return "Error: Command not allowed.";
}
(2) Spring Security 추가
관리자만 API에 접근 가능하도록 Spring Security를 설정합니다.
5. 테스트
1. 로컬에서 실행 테스트
• POST 요청으로 명령어를 전달하고 결과를 확인합니다.
curl -X POST http://localhost:8080/api/command/execute \
-H "Content-Type: application/json" \
-d '{"command": "ls"}'
2. 예외 상황 테스트
• 허용되지 않은 명령어를 실행하거나 잘못된 명령어를 테스트합니다.
6. 운영 환경 배포
1. 리눅스 환경에서 실행
• systemctl로 Spring Boot 애플리케이션을 서비스로 등록합니다.
2. 로그 관리
• 실행 결과를 로깅하거나 실패한 명령어 기록을 남깁니다.
3. 보안 강화
• IP 화이트리스트 설정 또는 인증 토큰 적용.
추가 고려사항
• 멀티스레드 처리: 동시에 여러 요청이 들어올 경우 동시 실행을 고려하세요.
• 실행 시간 제한: 특정 명령어가 너무 오래 실행되지 않도록 타임아웃을 설정하세요.
• 명령어 입력 데이터 검증: 명령어에 악성 입력(예: rm -rf /)이 들어가지 않도록 철저히 검증하세요.
필요하면 더 구체적인 구현을 도와줄게요!
네, 맞습니다! Spring Boot 에이전트에서 명령어를 실행하기 때문에 디렉토리 생성이나 쉘 스크립트를 실행하는 등의 작업을 충분히 수행할 수 있습니다. 하지만 이런 작업은 보안 및 입력 검증을 철저히 해야 합니다.
아래는 디렉토리 생성과 쉘 스크립트 실행을 예제로 설명하겠습니다.
1. 디렉토리 생성
mkdir 명령어를 통해 디렉토리를 생성할 수 있습니다.
요청 예시:
{
"command": "mkdir /tmp/new_directory"
}
실행 로직:
@PostMapping("/execute")
public String executeCommand(@RequestBody CommandRequest request) {
try {
String result = commandExecutor.executeCommand(request.getCommand());
return "Success: " + result;
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
테스트:
curl -X POST http://localhost:8080/api/command/execute \
-H "Content-Type: application/json" \
-d '{"command": "mkdir /tmp/new_directory"}'
2. 쉘 스크립트 실행
쉘 스크립트 파일을 실행하려면 bash script.sh 또는 ./script.sh 명령어를 사용할 수 있습니다.
요청 예시:
{
"command": "bash /path/to/your/script.sh"
}
실행 로직:
스크립트는 bash 명령어를 통해 실행됩니다. 스크립트 파일에 실행 권한이 필요하니, 권한 설정도 신경 써야 합니다.
@PostMapping("/execute-script")
public String executeShellScript(@RequestBody CommandRequest request) {
try {
return commandExecutor.executeCommand(request.getCommand());
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
테스트:
curl -X POST http://localhost:8080/api/command/execute \
-H "Content-Type: application/json" \
-d '{"command": "bash /path/to/your/script.sh"}'
3. 권한 및 검증
• 쉘 스크립트 권한 설정:
• 실행 전에 스크립트 파일에 실행 권한을 부여해야 합니다.
chmod +x /path/to/your/script.sh
• 명령어 제한:
• mkdir, bash와 같은 특정 명령어만 허용하도록 검증 로직을 추가하세요.
명령어 검증 코드:
private static final List<String> ALLOWED_COMMANDS = List.of("mkdir", "bash");
public static boolean isValidCommand(String command) {
return ALLOWED_COMMANDS.stream().anyMatch(command::startsWith);
}
4. 추가적인 보안 고려사항
1. 명령어 인젝션 방지:
• 사용자 입력을 검증하여 악의적인 명령어(rm -rf /, ;, && 포함)를 차단하세요.
2. 작업 디렉토리 제한:
• 특정 디렉토리(/tmp/ 또는 /opt/agent/)에서만 작업하도록 제한하세요.
3. 실행 결과 로깅:
• 실행 결과와 에러를 로그로 기록하여 문제 상황을 추적합니다.
5. 실행 예시
디렉토리 생성:
curl -X POST http://localhost:8080/api/command/execute \
-H "Content-Type: application/json" \
-d '{"command": "mkdir /tmp/test_directory"}'
쉘 스크립트 실행:
curl -X POST http://localhost:8080/api/command/execute \
-H "Content-Type: application/json" \
-d '{"command": "bash /tmp/test_script.sh"}'
이 에이전트를 이용하면 디렉토리 생성, 파일 복사, 쉘 스크립트 실행 등 다양한 작업을 수행할 수 있습니다. 추가로 구현하고 싶은 동작이나 보안 설정이 있다면 말씀해주세요!
맞습니다! 일반적으로 Spring 애플리케이션에서는 비즈니스 로직은 서비스 레이어에서 처리하고, 컨트롤러는 요청을 받아 서비스 레이어에 위임하는 방식으로 설계하는 것이 더 좋은 구조입니다. 이렇게 하면 레이어 분리가 명확해지고, 유지보수가 쉬워집니다.
아래는 서비스 레이어를 사용하는 구조로 개선한 예제입니다.
1. 레이어 설계
• Controller: 클라이언트 요청을 받아 서비스 레이어로 전달.
• Service: 명령어 실행 로직을 처리.
• CommandExecutor: 실제 명령어 실행 로직을 담당하는 유틸리티 클래스.
2. Controller 코드
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/api/command")
public class CommandController {
private final CommandService commandService;
@Autowired
public CommandController(CommandService commandService) {
this.commandService = commandService;
}
// 디렉토리 생성 요청
@PostMapping("/mkdir")
public String createDirectory(@RequestBody DirectoryRequest request) {
try {
commandService.createDirectory(request.getId());
return "Directory created: /tmp/" + request.getId();
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
// 스크립트 실행 요청
@PostMapping("/run-script")
public String runScript(@RequestBody ScriptRequest request) {
try {
commandService.runScript(request.getScriptName());
return "Script executed: /scripts/" + request.getScriptName();
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
}
3. Service 코드
CommandService는 비즈니스 로직을 처리하며, 명령어를 실행하기 위해 CommandExecutor를 호출합니다.
import org.springframework.stereotype.Service;
@Service
public class CommandService {
private final CommandExecutor commandExecutor;
public CommandService() {
this.commandExecutor = new CommandExecutor();
}
// 디렉토리 생성 비즈니스 로직
public void createDirectory(String id) throws Exception {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("Directory ID cannot be null or empty");
}
String directoryPath = "/tmp/" + id; // 고정된 디렉토리 경로
String command = "mkdir -p " + directoryPath;
commandExecutor.executeCommand(command);
}
// 스크립트 실행 비즈니스 로직
public void runScript(String scriptName) throws Exception {
if (scriptName == null || scriptName.isEmpty()) {
throw new IllegalArgumentException("Script name cannot be null or empty");
}
// 허용된 스크립트만 실행
if (!isValidScript(scriptName)) {
throw new SecurityException("Unauthorized script: " + scriptName);
}
String scriptPath = "/scripts/" + scriptName; // 고정된 스크립트 경로
String command = "bash " + scriptPath;
commandExecutor.executeCommand(command);
}
// 스크립트 검증 로직
private boolean isValidScript(String scriptName) {
return List.of("example_script.sh", "cleanup.sh").contains(scriptName);
}
}
4. CommandExecutor 코드
명령어를 실제로 실행하는 로직은 기존과 동일하게 CommandExecutor에서 처리합니다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class CommandExecutor {
public void executeCommand(String command) throws Exception {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", command);
Process process = processBuilder.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 명령어 출력 로그
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed with exit code " + exitCode);
}
}
}
5. 요청 DTO
기존과 동일하게 요청 데이터를 처리하는 DTO 클래스입니다.
class DirectoryRequest {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
class ScriptRequest {
private String scriptName;
public String getScriptName() {
return scriptName;
}
public void setScriptName(String scriptName) {
this.scriptName = scriptName;
}
}
6. 이 구조의 장점
1. 레이어 분리:
• Controller는 요청 처리에만 집중하고, 서비스는 비즈니스 로직을 담당하므로 역할이 명확히 분리됩니다.
2. 유지보수성 향상:
• 서비스 레이어에서 로직을 변경해도 컨트롤러 코드에는 영향을 미치지 않습니다.
• 테스트 및 로깅이 더 간단해집니다.
3. 확장성:
• 추가 비즈니스 로직이 필요할 경우 서비스 레이어에서 처리하면 됩니다.
• 예를 들어, 디렉토리 생성 전에 권한 체크를 추가하거나, 실행 결과를 데이터베이스에 저장하는 기능을 쉽게 확장할 수 있습니다.
7. 요청 테스트
(1) 디렉토리 생성 요청
Request URL:
POST http://localhost:8080/api/command/mkdir
Request Body:
{
"id": "test_directory"
}
결과:
/tmp/test_directory 디렉토리가 생성됩니다.
(2) 쉘 스크립트 실행 요청
Request URL:
POST http://localhost:8080/api/command/run-script
Request Body:
{
"scriptName": "example_script.sh"
}
결과:
/scripts/example_script.sh 스크립트가 실행됩니다.
이 구조를 기반으로 설계하면 확장성과 유지보수성을 모두 확보할 수 있습니다. 추가적으로 보안 설정이나 로그 저장이 필요하면 알려주세요!