Skip to content
Snippets Groups Projects
Commit b96e7708 authored by 천 진강's avatar 천 진강
Browse files

포트포워딩 CRUD

parent e92e5bd4
Branches
No related tags found
3 merge requests!15Feat/certificate,!7Feat/forwarding 포트포워딩 CRUD,!6Feat/forwarding 포트포워딩 CRUD
Showing
with 402 additions and 17 deletions
......@@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
runtimeOnly 'com.mysql:mysql-connector-j'
......
package com.aolda.itda.config;
import com.aolda.itda.exception.CustomException;
import com.aolda.itda.exception.ErrorCode;
import com.aolda.itda.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@RequiredArgsConstructor
@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
private final AuthService authService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("X-Subject-Token");
if (token == null || token.isEmpty()) {
throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
}
// 어드민과 일반 유저 구분 필요
try {
if (authService.validateTokenAndGetUserId(token) != null) {
return true;
}
} catch (Exception e) {
log.error("Token validation failed for URI {}: {}", request.getRequestURI(), e.getMessage(), e);
throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
}
throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
}
}
package com.aolda.itda.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
registry.addMapping("/**")
......@@ -16,4 +22,13 @@ public class WebConfig implements WebMvcConfigurer {
.exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin")
;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] excludeAuth = {"/error", "/api/auth/*" };
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludeAuth);
}
}
package com.aolda.itda.controller.forwarding;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.service.forwarding.ForwardingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ForwardingController {
private final ForwardingService forwardingService;
@PostMapping("/forwarding")
public ResponseEntity<Object> create(@RequestParam String projectId,
@RequestBody ForwardingDTO dto) {
forwardingService.createForwarding(projectId, dto);
return ResponseEntity.ok().build();
}
@GetMapping("/forwarding")
public ResponseEntity<Object> view(@RequestParam Long forwardingId) {
return ResponseEntity.ok(forwardingService.getForwarding(forwardingId));
}
@GetMapping("/forwardings")
public ResponseEntity<Object> lists(@RequestParam String projectId) {
return ResponseEntity.ok(forwardingService.getForwardings(projectId));
}
@PatchMapping("/forwarding")
public ResponseEntity<Object> edit(@RequestParam Long forwardingId,
@RequestBody ForwardingDTO dto) {
forwardingService.editForwarding(forwardingId, dto);
return ResponseEntity.ok().build();
}
@DeleteMapping("/forwarding")
public ResponseEntity<Object> delete(@RequestParam Long forwardingId) {
forwardingService.deleteForwarding(forwardingId);
return ResponseEntity.ok().build();
}
}
package com.aolda.itda.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PageResp<T> {
private Integer totalPages;
private Integer totalElements;
private Integer size;
private List<T> contents;
private Boolean first;
private Boolean last;
public static <T> PageRespBuilder<T> builderFor(Class<T> clazz) {
return (PageRespBuilder<T>) new PageRespBuilder<>();
}
}
package com.aolda.itda.dto.forwarding;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ForwardingDTO {
private Long id;
@Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
message = "잘못된 IP 형식 (server)")
private String serverIp;
@NotBlank(message = "serverPort 값이 존재하지 않습니다")
@Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
message = "잘못된 포트 형식 (server)")
private String serverPort;
@NotBlank(message = "instanceIp 값이 존재하지 않습니다")
@Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
message = "잘못된 IP 형식 (instance)")
private String instanceIp;
@NotBlank(message = "instancePort 값이 존재하지 않습니다")
@Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
message = "잘못된 포트 형식 (instance)")
private String instancePort;
@NotBlank(message = "name 값이 존재하지 않습니다")
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
......@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "routing")
@Table(name = "certificate")
@Getter
@Builder
@NoArgsConstructor
......@@ -40,7 +40,7 @@ public class Certificate extends BaseTimeEntity {
private Boolean isDeleted;
private String metadata;
private String description;
}
package com.aolda.itda.entity.forwarding;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.entity.BaseTimeEntity;
import com.aolda.itda.entity.user.User;
import jakarta.persistence.*;
......@@ -36,4 +37,35 @@ public class Forwarding extends BaseTimeEntity {
private String instancePort;
private Boolean isDeleted;
private String name;
public ForwardingDTO toForwardingDTO() {
return ForwardingDTO.builder()
.id(forwardingId)
.name(name)
.serverPort(serverPort)
.instanceIp(instanceIp)
.instancePort(instancePort)
.createdAt(getCreatedAt())
.updatedAt(getUpdatedAt())
.build();
}
public void edit(ForwardingDTO dto) {
this.name = dto.getName() != null ? dto.getName() : this.name;
this.serverPort = dto.getServerPort() != null &&
dto.getServerPort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
? dto.getServerPort() : this.serverPort;
this.instanceIp = dto.getInstanceIp() != null &&
dto.getInstanceIp().matches("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$")
? dto.getInstanceIp() : this.instanceIp;
this.instancePort = dto.getInstancePort() != null &&
dto.getInstancePort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
? dto.getInstancePort() : this.instancePort;
}
public void delete() {
this.isDeleted = true;
}
}
......@@ -27,7 +27,7 @@ public class Routing extends BaseTimeEntity {
private User user;
@OneToOne
@JoinColumn(name = "certificate_id")
@JoinColumn(name = "certificate_id", nullable = false)
private Certificate certificate;
private String projectId;
......@@ -38,5 +38,6 @@ public class Routing extends BaseTimeEntity {
private Boolean isDeleted;
private String description;
}
......@@ -12,7 +12,14 @@ public enum ErrorCode {
INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
// Token
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다");
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
//Forwarding
FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다");
private final HttpStatus status;
private final String message;
......
package com.aolda.itda.repository.forwarding;
import com.aolda.itda.entity.forwarding.Forwarding;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface ForwardingRepository extends JpaRepository<Forwarding, Long> {
List<Forwarding> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
Optional<Forwarding> findByForwardingIdAndIsDeleted(Long forwardingId, Boolean isDeleted);
Boolean existsByInstanceIpAndInstancePortAndIsDeleted(String instanceIp, String instancePort, Boolean isDeleted);
Boolean existsByServerPortAndIsDeleted(String serverPort, Boolean isDeleted);
}
......@@ -15,7 +15,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.util.*;
......@@ -79,8 +78,13 @@ public class AuthService {
"}";
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> res = restTemplate.postForEntity(url, requestEntity, Map.class);
ResponseEntity<Map> res;
try {
res = restTemplate.postForEntity(url, requestEntity, Map.class);
} catch (Exception e) {
e.printStackTrace();
throw new CustomException(ErrorCode.INVALID_USER_INFO);
}
Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
String userId = (String) resUser.get("id");
......@@ -263,7 +267,7 @@ public class AuthService {
return lists;
}
private String validateTokenAndGetUserId(String token) throws JsonProcessingException {
public String validateTokenAndGetUserId(String token) throws JsonProcessingException {
String url = keystone + "/auth/tokens";
HttpHeaders headers = new HttpHeaders();
headers.set("X-Auth-Token", token);
......@@ -273,7 +277,6 @@ public class AuthService {
try {
res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
} catch (HttpClientErrorException.NotFound e) {
System.out.println("validate");
throw new CustomException(ErrorCode.INVALID_TOKEN);
}
return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
......@@ -290,18 +293,14 @@ public class AuthService {
res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
} catch (RuntimeException e) {
e.printStackTrace();
System.out.println("runtime");
return false;
}
JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments");
String system_all = node.path("scope").path("system").path("all").asText();
String role = node.path("role").path("name").asText();
System.out.println("role: " + role);
if (system_all.equals("true") && role.equals("admin")) {
System.out.println(system_all);
return true;
}
System.out.println("hi");
return false;
}
......
package com.aolda.itda.service.forwarding;
import com.aolda.itda.dto.PageResp;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.entity.forwarding.Forwarding;
import com.aolda.itda.exception.CustomException;
import com.aolda.itda.exception.ErrorCode;
import com.aolda.itda.repository.forwarding.ForwardingRepository;
import com.aolda.itda.template.ForwardingTemplate;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
@Transactional
@RequiredArgsConstructor
public class ForwardingService {
@Value("${spring.server.base-ip}")
private String serverBaseIp;
private final ForwardingTemplate forwardingTemplate;
private final ForwardingRepository forwardingRepository;
/* 포트포워딩 정보 조회 */
public ForwardingDTO getForwarding(Long forwardingId) {
Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
return forwarding.toForwardingDTO();
}
/* 포트포워딩 목록 조회 */
public PageResp<ForwardingDTO> getForwardings(String projectId) {
return PageResp.<ForwardingDTO>builder()
.contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false)
.stream()
.map(Forwarding::toForwardingDTO)
.toList()).build();
}
/* 포트포워딩 생성 */
public void createForwarding(String projectId, ForwardingDTO dto) {
/* 입력 DTO 검증 */
validateDTO(dto);
/* 중복 검증 */
if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
}
if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
}
/* 포트포워딩 엔티티 생성 */
Forwarding forwarding = Forwarding.builder()
.isDeleted(false)
.projectId(projectId)
.name(dto.getName())
.serverIp(dto.getServerIp() == null ? serverBaseIp : dto.getServerIp())
.serverPort(dto.getServerPort())
.instanceIp(dto.getInstanceIp())
.instancePort(dto.getInstancePort())
.build();
forwardingRepository.save(forwarding);
/* nginx conf 파일 생성 및 예외 처리 */
String content = forwardingTemplate.getPortForwardingWithTCP(dto.getServerPort(), dto.getInstanceIp(), dto.getInstancePort(), dto.getName());
String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
File file = new File(confPath);
try {
Path path = Paths.get(confPath);
Files.createDirectories(path.getParent());
if (!file.createNewFile()) {
throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다");
}
} catch (IOException e) {
e.printStackTrace();
throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
}
/* conf 파일 작성 및 예외 처리 */
try {
BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요
bw.write(content);
bw.flush();
bw.close();
} catch (Exception e) {
e.printStackTrace();
if (file.exists()) {
file.delete();
}
throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
}
}
/* 포트포워딩 정보 수정 */
public void editForwarding(Long forwardingId, ForwardingDTO dto) {
Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
/* 중복 검증 */
if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
}
if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
}
/* 정보 수정 */
forwarding.edit(dto);
forwardingRepository.save(forwarding);
}
/* 포트포워딩 삭제 (소프트) */
public void deleteForwarding(Long forwardingId) {
Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
/* 파일 삭제 */
String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
File file = new File(confPath);
if (!file.delete()) {
throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 삭제할 수 없습니다");
}
/* DB */
forwarding.delete();
forwardingRepository.save(forwarding);
}
/* 입력 DTO 검증 */
private void validateDTO(ForwardingDTO dto) {
for (ConstraintViolation<ForwardingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
}
}
}
......@@ -5,9 +5,12 @@ import org.springframework.stereotype.Component;
@Component
public class ForwardingTemplate {
public String getPortForwardingWithTCP(String instanceIp, String serverPort) {
return "\nlisten " + serverPort + "; \n" +
public String getPortForwardingWithTCP(String serverPort, String instanceIp, String instancePort, String name) {
return "# " + name + "\n" +
"server { \n" +
" listen " + serverPort + "; \n" +
" listen [::]:" + serverPort + "; \n" +
"proxy_pass " + instanceIp + ";\n";
" proxy_pass " + instanceIp + ":" + instancePort + ";\n" +
"} \n";
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment