From efc03bac91449ef6dd80d37f4d193615f79b9f7d Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Mon, 29 Dec 2025 17:18:17 +0900 Subject: [PATCH] ... --- .../lun/configs/security/SecurityConfig.kt | 8 +- .../controllers/SynologyProxyController.kt | 130 ++++++++++ .../controllers/view/PostViewController.kt | 5 + .../templates/content/slideshow.html | 234 ++++++++++++++++++ .../resources/templates/fragments/header.html | 3 + 5 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/controllers/SynologyProxyController.kt create mode 100644 src/main/resources/templates/content/slideshow.html diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/security/SecurityConfig.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/security/SecurityConfig.kt index dd19088..8c43115 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/security/SecurityConfig.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/security/SecurityConfig.kt @@ -112,6 +112,7 @@ class SecurityConfig( .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // API는 세션을 사용하지 않음 .authorizeHttpRequests { auth -> auth + .requestMatchers("/api/synology/**").permitAll() .requestMatchers(HttpMethod.GET,"/api/feed").permitAll() .requestMatchers("/api/stock/**").permitAll() .requestMatchers("/api/ranks/**").permitAll() @@ -167,6 +168,7 @@ class SecurityConfig( .requestMatchers( "/webfonts/**", "/css/**", "/js/**", "/assets/**", "/webjars/**" ).permitAll() + .requestMatchers("/api/synology/**").permitAll() .requestMatchers(HttpMethod.GET,"/stock/**").permitAll() // 2. 공개 GET API 및 페이지 = permitAll .requestMatchers(HttpMethod.GET, @@ -182,12 +184,14 @@ class SecurityConfig( "/puzzle/images/**", "/bums/face.bs", // [추가] 사이트 소개 페이지 "/bookmarks/**", // [추가] 북마크 목록 페이지 - "/ads.txt" + "/ads.txt", + "/slideshow" ).permitAll() // 3. 공개 POST API = permitAll .requestMatchers(HttpMethod.POST, - "/user/login.bjx", "/user/joinUser.bjx", + "/user/login.bjx", + "/user/joinUser.bjx", "/api/ranks/submit", "/bums/save/loc.api", "/puzzle/**", diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SynologyProxyController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SynologyProxyController.kt new file mode 100644 index 0000000..0909043 --- /dev/null +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/SynologyProxyController.kt @@ -0,0 +1,130 @@ +package kr.lunaticbum.back.lun.controllers.api + +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.ssl.util.InsecureTrustManagerFactory +import kotlinx.coroutines.reactor.awaitSingle +import org.slf4j.LoggerFactory +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.bind.annotation.* +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBody +import reactor.core.publisher.Flux +import reactor.netty.http.client.HttpClient + +@RestController +@RequestMapping("/api/synology") +class SynologyProxyController { + + private val logger = LoggerFactory.getLogger(SynologyProxyController::class.java) + + // SSL 검증 무시 설정 (HTTPS 사용 시 필요) + private val httpClient = HttpClient.create() + .secure { t -> t.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build() + )} + + // [수정됨] WebClient 생성 시 버퍼 크기 제한 해제 (256KB -> 20MB) + private val webClient = WebClient.builder() + .clientConnector(ReactorClientHttpConnector(httpClient)) + .codecs { configurer -> + configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) // 20MB로 증설 + } + .build() + + private fun buildBaseUrl(nasAddress: String): String { + if (nasAddress.startsWith("http://") || nasAddress.startsWith("https://")) { + return nasAddress + } + return if (nasAddress.contains(":5000")) "http://$nasAddress" else "https://$nasAddress" + } + + @PostMapping("/login") + suspend fun login(@RequestBody payload: Map): String { + try { + val nasAddress = payload["nasAddress"].toString() + val username = payload["username"].toString() + val password = payload["password"].toString() + + val baseUrl = buildBaseUrl(nasAddress) + val url = "$baseUrl/webapi/auth.cgi?api=SYNO.API.Auth&version=7&method=login&account=$username&passwd=$password&session=SynologyPhotos&format=sid" + + logger.info("Login Request URL: $url") + return webClient.get().uri(url).retrieve().awaitBody() + } catch (e: Exception) { + logger.error("Login Error: ", e) + throw e + } + } + + @PostMapping("/list") + suspend fun list(@RequestBody payload: Map): String { + try { + val nasAddress = payload["nasAddress"].toString() + val sid = payload["sid"].toString() + val offset = payload["offset"].toString() + val limit = payload["limit"].toString() + + val baseUrl = buildBaseUrl(nasAddress) + val url = "$baseUrl/webapi/entry.cgi?api=SYNO.Foto.Browse.Item&version=1&method=list&limit=$limit&offset=$offset&_sid=$sid" + + logger.info("List Request URL: $url") + return webClient.get().uri(url).retrieve().awaitBody() + } catch (e: Exception) { + logger.error("List Error: ", e) + throw e + } + } + + @GetMapping("/image") + suspend fun getImage( + @RequestParam nasAddress: String, + @RequestParam sid: String, + @RequestParam id: String + ): ResponseEntity { + val baseUrl = buildBaseUrl(nasAddress) + + // [핵심] 문자열 더하기 대신 UriComponentsBuilder 사용 + // Spring이 '[' 문자를 자동으로 알맞게(%5B) 인코딩해줍니다. (이중 인코딩 방지) + val uri = org.springframework.web.util.UriComponentsBuilder.fromHttpUrl(baseUrl) + .path("/webapi/entry.cgi") + .queryParam("api", "SYNO.Foto.Download") + .queryParam("version", "1") + .queryParam("method", "download") + .queryParam("force_download", "true") + .queryParam("item_id", "[$id]") // 여기에 대괄호 []를 직접 넣습니다. + .queryParam("_sid", sid) + .build() + .toUri() + + try { + val response = webClient.get() + .uri(uri) // String url 대신 URI 객체를 전달 + .accept(MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG, MediaType.ALL) + .retrieve() + .toEntity(ByteArray::class.java) + .awaitSingle() + + val contentType = response.headers.contentType + + // 에러 체크 + if (contentType != null && !contentType.toString().startsWith("image")) { + val errorBody = String(response.body ?: ByteArray(0)) + logger.error("NAS returned non-image content ($contentType): $errorBody") + logger.error("Requested URI: $uri") // 디버깅을 위해 요청한 URI도 출력 + } + + return ResponseEntity.ok() + .contentType(contentType ?: MediaType.IMAGE_JPEG) + .body(response.body) + + } catch (e: Exception) { + logger.error("Image Fetch Error: ", e) + return ResponseEntity.notFound().build() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt index a03219b..2141c04 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/view/PostViewController.kt @@ -246,6 +246,11 @@ class PostViewController( return vm } + @GetMapping("/slideshow") + fun slideshowPage(): ResultMV { + return ResultMV("content/slideshow") // templates/content/slideshow.html + } + // --- [핵심 수정] 뷰어 (북마크 포함) --- // [핵심 수정] 뷰어 메서드 @GetMapping("/blog/viewer/{id}") diff --git a/src/main/resources/templates/content/slideshow.html b/src/main/resources/templates/content/slideshow.html new file mode 100644 index 0000000..32a8c3b --- /dev/null +++ b/src/main/resources/templates/content/slideshow.html @@ -0,0 +1,234 @@ + + + + + + BUM's NAS Slideshow + + + +✕ Close + +
+
+

Synology Login

+ + + + + + + + +

+
+
+ +
+
+ +
+ Speed: 5 sec + +
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 406a5e3..33489f1 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -14,6 +14,8 @@