Skip to content
Snippets Groups Projects
Commit 3925e264 authored by nahyun's avatar nahyun
Browse files

Merge branch 'feat/routing' into 'dev'

Feat/routing 라우팅 CRUD 작성

See merge request !10
parents 21125ed0 783724ad
No related branches found
No related tags found
2 merge requests!15Feat/certificate,!10Feat/routing 라우팅 CRUD 작성
Showing
with 529 additions and 11 deletions
......@@ -25,7 +25,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] excludeAuth = {"/error", "/api/auth/*" };
String[] excludeAuth = {"/error", "/api/auth/*", "/api/*" };
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
......
......@@ -2,6 +2,7 @@ package com.aolda.itda.controller.forwarding;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.service.forwarding.ForwardingService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
......
package com.aolda.itda.controller.routing;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.dto.routing.RoutingDTO;
import com.aolda.itda.service.routing.RoutingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class RoutingController {
private final RoutingService routingService;
@PostMapping("/forwarding")
public ResponseEntity<Object> create(@RequestParam String projectId,
@RequestBody RoutingDTO dto) {
routingService.createRouting(projectId, dto);
return ResponseEntity.ok().build();
}
@GetMapping("/routing")
public ResponseEntity<Object> view(@RequestParam Long routingId) {
return ResponseEntity.ok(routingService.getRouting(routingId));
}
@GetMapping("/routings")
public ResponseEntity<Object> lists(@RequestParam String projectId) {
return ResponseEntity.ok(routingService.getRoutings(projectId));
}
@PatchMapping("/routing")
public ResponseEntity<Object> edit(@RequestParam Long routingId,
@RequestBody RoutingDTO dto) {
routingService.editRouting(routingId, dto);
return ResponseEntity.ok().build();
}
@DeleteMapping("/routing")
public ResponseEntity<Object> delete(@RequestParam Long routingId) {
routingService.deleteRouting(routingId);
return ResponseEntity.ok().build();
}
}
package com.aolda.itda.dto.routing;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
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 RoutingDTO {
@NotBlank
private String ip;
@NotBlank
private String domain;
@NotBlank
private String name;
@NotBlank
private String port;
private Long id;
@NotBlank
private Long certificateId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@NotNull
private Boolean caching;
}
......@@ -42,5 +42,7 @@ public class Certificate extends BaseTimeEntity {
private String description;
public String formatDomain() {
return domain == null ? null : domain.replace("*", "_");
}
}
......@@ -35,6 +35,6 @@ public class Log extends BaseTimeEntity {
@Enumerated(EnumType.STRING)
private Action action;
private String metadata;
private String description;
}
package com.aolda.itda.entity.routing;
import com.aolda.itda.dto.forwarding.ForwardingDTO;
import com.aolda.itda.dto.routing.RoutingDTO;
import com.aolda.itda.entity.BaseTimeEntity;
import com.aolda.itda.entity.certificate.Certificate;
import com.aolda.itda.entity.user.User;
......@@ -36,8 +38,38 @@ public class Routing extends BaseTimeEntity {
private String instanceIp;
private String instancePort;
private Boolean isDeleted;
private String description;
private Boolean caching;
private String name;
public RoutingDTO toRoutingDTO() {
return RoutingDTO.builder()
.id(routingId)
.name(name)
.port(instancePort)
.ip(instanceIp)
.certificateId(certificate == null ? null : certificate.getCertificateId())
.caching(caching)
.domain(domain)
.createdAt(getCreatedAt())
.updatedAt(getUpdatedAt())
.build();
}
public void edit(RoutingDTO dto, Certificate certificate) {
this.name = dto.getName() != null ? dto.getName() : this.name;
this.instanceIp = dto.getIp() != null ? dto.getIp() : this.instanceIp;
this.instancePort = dto.getPort() != null ? dto.getPort() : this.instancePort;
this.caching = dto.getCaching() != null ? dto.getCaching() : this.caching;
this.domain = dto.getDomain() != null ? dto.getDomain() : this.domain;
this.certificate = certificate;
}
public void delete() {
this.isDeleted = true;
}
}
......@@ -15,12 +15,21 @@ public enum ErrorCode {
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
// Forwarding
NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
// Routing
NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"),
// Certificate
NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"),
// CONF File
FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
FAIL_UPDATE_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, "중복된 서버 포트입니다"),
DUPLICATED_DOMAIN_NAME(HttpStatus.BAD_REQUEST, "중복된 도메인 주소입니다"),
// Nginx
FAIL_NGINX_CONF_TEST(HttpStatus.BAD_REQUEST, "Conf 파일 테스트에 실패했습니다"),
......
package com.aolda.itda.repository.certificate;
import com.aolda.itda.entity.certificate.Certificate;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CertificateRepository extends JpaRepository<Certificate, Long> {
}
package com.aolda.itda.repository.routing;
import com.aolda.itda.entity.forwarding.Forwarding;
import com.aolda.itda.entity.routing.Routing;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface RoutingRepository extends JpaRepository<Routing, Long> {
List<Routing> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
Optional<Routing> findByRoutingIdAndIsDeleted(Long routingId, Boolean isDeleted);
Boolean existsByDomainAndIsDeleted(String domain, Boolean isDeleted);
}
package com.aolda.itda.service.routing;
import com.aolda.itda.dto.PageResp;
import com.aolda.itda.dto.routing.RoutingDTO;
import com.aolda.itda.entity.certificate.Certificate;
import com.aolda.itda.entity.routing.Routing;
import com.aolda.itda.exception.CustomException;
import com.aolda.itda.exception.ErrorCode;
import com.aolda.itda.repository.certificate.CertificateRepository;
import com.aolda.itda.repository.routing.RoutingRepository;
import com.aolda.itda.template.RoutingTemplate;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
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;
import java.nio.file.StandardCopyOption;
@Service
@Transactional
@RequiredArgsConstructor
@Slf4j
public class RoutingService {
private final RoutingRepository routingRepository;
private final CertificateRepository certificateRepository;
private final RoutingTemplate routingTemplate;
private final RestTemplate restTemplate = new RestTemplate();
/* Routing 조회 */
public RoutingDTO getRouting(Long routingId) {
// project id 확인 필요
Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
return routing.toRoutingDTO();
}
/* Routing 목록 조회 */
public PageResp<RoutingDTO> getRoutings(String projectId) {
// project id 확인 필요
return PageResp.<RoutingDTO>builder()
.contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
.stream()
.map(Routing::toRoutingDTO)
.toList()).build();
}
/* Routing 생성 */
public void createRouting(String projectId, RoutingDTO dto) {
/* 입력 DTO 검증 */
validateDTO(dto);
/* 중복 검증 */
if (routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
}
/* SSL 인증서 조회 */
Certificate certificate = dto.getCertificateId() == -1 ? null :
certificateRepository.findById(dto.getCertificateId())
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
/* 라우팅 엔티티 생성 */
Routing routing = Routing.builder()
.isDeleted(false)
.projectId(projectId)
.name(dto.getName())
.instanceIp(dto.getIp())
.instancePort(dto.getPort())
.domain(dto.getDomain())
.certificate(certificate)
.caching(dto.getCaching())
.build();
routingRepository.save(routing);
/* nginx conf 파일 생성 및 예외 처리 */
String content = routingTemplate.getRouting(dto, certificate == null ? null : certificate.formatDomain());
String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".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, false)); // 예외처리 필요
bw.write(content);
bw.flush();
bw.close();
} catch (Exception e) {
e.printStackTrace();
if (file.delete()) {
throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
}
throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
}
/* nginx test */
String url = "http://nginx:8081/nginx-api/test";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
if (file.delete()) {
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
} catch (Exception e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
if (file.delete()) {
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
}
/* nginx reload */
url = "http://nginx:8081/nginx-api/reload";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
if (file.delete()) {
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
} catch (Exception e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
if (file.delete()) {
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
}
}
/* Routing 수정 */
public void editRouting(Long routingId, RoutingDTO dto) {
Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
/* 입력 DTO 검증 */
validateDTO(dto);
/* 중복 검증 */
if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
}
/* SSL 인증서 조회 */
Certificate certificate = (dto.getCertificateId() == null) || (dto.getCertificateId() == -1 ) ? null :
certificateRepository.findById(dto.getCertificateId())
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
/* 파일 수정 */
routing.edit(dto, certificate);
String content = routingTemplate.getRouting(routing.toRoutingDTO(), certificate == null ? null : certificate.formatDomain());
String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
File file = new File(confPath);
if (!file.exists()) {
throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
}
Path backup;
try {
backup = Files.createTempFile("temp_", ".tmp");
Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING
, StandardCopyOption.COPY_ATTRIBUTES);
BufferedWriter bw = new BufferedWriter(new FileWriter(file, false));
bw.write(content);
bw.flush();
bw.close();
} catch (Exception e) {
e.printStackTrace();
throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다");
}
/* nginx test */
String url = "http://nginx:8081/nginx-api/test";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
try {
Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
, StandardCopyOption.COPY_ATTRIBUTES);
Files.delete(backup);
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
} catch (RuntimeException e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
try {
Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
, StandardCopyOption.COPY_ATTRIBUTES);
Files.delete(backup);
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
}
/* nginx reload */
url = "http://nginx:8081/nginx-api/reload";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
try {
Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
, StandardCopyOption.COPY_ATTRIBUTES);
Files.delete(backup);
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
} catch (RuntimeException e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
try {
Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
, StandardCopyOption.COPY_ATTRIBUTES);
Files.delete(backup);
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
}
/* DB 정보 수정 */
routingRepository.save(routing);
}
/* Routing 삭제 */
public void deleteRouting(Long routingId) {
Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
/* 파일 삭제 */
String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
String deletePath = confPath + ".deleted";
try {
Files.move(Paths.get(confPath), Paths.get(deletePath));
} catch (IOException e) {
throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
}
/* nginx test */
String url = "http://nginx:8081/nginx-api/test";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
try {
Files.move(Paths.get(deletePath), Paths.get(confPath));
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
} catch (Exception e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
try {
Files.move(Paths.get(deletePath), Paths.get(confPath));
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
}
/* nginx reload */
url = "http://nginx:8081/nginx-api/reload";
try {
restTemplate.getForEntity(url, String.class);
} catch (HttpServerErrorException.InternalServerError e) {
log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
try {
Files.move(Paths.get(deletePath), Paths.get(confPath));
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
} catch (Exception e) {
log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
try {
Files.move(Paths.get(deletePath), Paths.get(confPath));
} catch (IOException e1) {
throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
}
throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
}
/* DB */
routing.delete();
routingRepository.save(routing);
}
private void validateDTO(RoutingDTO dto) {
for (ConstraintViolation<RoutingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
}
}
}
......@@ -5,22 +5,22 @@ import org.springframework.stereotype.Component;
@Component
public class OptionTemplate {
public String getSSL(Long certificateId) {
public String getSSL(String certificateDomain) {
return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" +
"include conf.d/include/ssl-ciphers.conf;\n" +
"ssl_certificate /etc/letsencrypt/live/npm-" + certificateId + "/fullchain.pem;\n" +
"ssl_certificate_key /etc/letsencrypt/live/npm-" + certificateId + "/privkey.pem;\n";
"ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" +
"ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n";
}
public String getAssetCaching() {
return "include conf.d/include/assets.conf;\n";
return "\ninclude conf.d/include/assets.conf;\n";
}
public String getBlockExploits() {
return "include conf.d/include/block-exploits.conf;\n";
return "\ninclude conf.d/include/block-exploits.conf;\n";
}
public String getForceSSL() {
return "include conf.d/include/force-ssl.conf;\n";
return "\ninclude conf.d/include/force-ssl.conf;\n";
}
}
package com.aolda.itda.template;
import com.aolda.itda.dto.routing.RoutingDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class RoutingTemplate {
private final OptionTemplate optionTemplate;
public String getRouting(RoutingDTO dto, String certificateDomain) {
return "server { \n"
+ "set $forward_scheme http;\n"
+ "set $server \"" + dto.getIp() +"\";\n"
+ "set $port "+ dto.getPort() +";\n"
+ "\n"
+ "listen 80;\n"
+ "listen [::]:80;\n"
+ (dto.getCertificateId() == -1 ? "" :
"listen 443 ssl;\n listen [::]:443 ssl;\n")
+ "server_name " + dto.getDomain() + ";\n"
+ (dto.getCertificateId() == -1 ? "" :
optionTemplate.getSSL(certificateDomain))
+ (dto.getCaching() ? "" :
optionTemplate.getAssetCaching()
)
+ "proxy_set_header Upgrade $http_upgrade;\n"
+ "proxy_set_header Connection $http_connection;\n"
+ "proxy_http_version 1.1;\n"
+ "\n"
+ "access_log /data/logs/proxy-host-" + dto.getId() + "_access.log proxy;\n"
+ "error_log /data/logs/proxy-host-" + dto.getId() + "_error.log warn;\n"
+ "location / { \n"
+ "proxy_set_header Upgrade $http_upgrade;\n"
+ "proxy_set_header Connection $http_connection;\n"
+ "proxy_http_version 1.1;\n"
+ "include conf.d/include/proxy.conf;\n"
+ "}\n"
+ "}\n";
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment