From 1db35b9088789a0871baab637589c292cdc0a27a Mon Sep 17 00:00:00 2001 From: litoral05 Date: Tue, 5 May 2026 15:47:24 +0100 Subject: [PATCH] Adds Download endpoint for client bundle --- .../openvpn/OpenVpnBundleDownload.java | 10 +++ .../openvpn/openvpn/OpenVpnService.java | 9 +++ .../openvpn/router/RouterController.java | 33 ++++++++- .../litoralregas/openvpn/ssh/SshService.java | 69 ++++++++++++++++++ test-router.tar.gz | Bin 0 -> 5584 bytes 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnBundleDownload.java create mode 100644 test-router.tar.gz diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnBundleDownload.java b/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnBundleDownload.java new file mode 100644 index 0000000..6094cb5 --- /dev/null +++ b/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnBundleDownload.java @@ -0,0 +1,10 @@ +package com.litoralregas.openvpn.openvpn; + +import java.io.InputStream; + +public record OpenVpnBundleDownload( + String filename, + long size, + InputStream inputStream +) { +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnService.java b/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnService.java index 7677d91..2d76f6a 100644 --- a/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnService.java +++ b/src/main/java/com/litoralregas/openvpn/openvpn/OpenVpnService.java @@ -137,4 +137,13 @@ public class OpenVpnService { return sshService.executeOnConfiguredVps(command); } + + public OpenVpnBundleDownload downloadClientBundle(String clientName) { + String filename = clientName + ".tar.gz"; + + return sshService.downloadFileFromConfiguredVps( + "/var/litoral_regas_openvpn/clients/" + filename, + filename + ); + } } \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/router/RouterController.java b/src/main/java/com/litoralregas/openvpn/router/RouterController.java index f969d3d..44f94b5 100644 --- a/src/main/java/com/litoralregas/openvpn/router/RouterController.java +++ b/src/main/java/com/litoralregas/openvpn/router/RouterController.java @@ -7,6 +7,10 @@ import com.litoralregas.openvpn.openvpn.IpAllocationService; import com.litoralregas.openvpn.openvpn.OpenVpnService; import com.litoralregas.openvpn.ssh.SshService; import jakarta.validation.Valid; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -18,14 +22,12 @@ public class RouterController { private final RouterService service; private final DeploymentService deploymentService; - private final SshService sshService; private final IpAllocationService ipAllocationService; private final OpenVpnService openVpnService; - public RouterController(RouterService service, DeploymentService deploymentService, SshService sshService, IpAllocationService ipAllocationService, OpenVpnService openVpnService) { + public RouterController(RouterService service, DeploymentService deploymentService, IpAllocationService ipAllocationService, OpenVpnService openVpnService) { this.service = service; this.deploymentService = deploymentService; - this.sshService = sshService; this.ipAllocationService = ipAllocationService; this.openVpnService = openVpnService; } @@ -45,7 +47,6 @@ public class RouterController { return service.findById(id); } - @DeleteMapping("/{id}") public void delete(@PathVariable UUID id) { service.delete(id); @@ -154,4 +155,28 @@ public class RouterController { return DeploymentResponse.from(failed); } } + + @GetMapping("/{id}/bundle") + public ResponseEntity downloadBundle(@PathVariable UUID id) { + Router router = service.findById(id); + + if (router.getStatus() != RouterStatus.PROVISIONED) { + throw new IllegalStateException("Router is not provisioned yet"); + } + + var allocation = ipAllocationService.findByRouterId(id); + + var bundle = openVpnService.downloadClientBundle( + allocation.getClientName() + ); + + return ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + bundle.filename() + "\"" + ) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(bundle.size()) + .body(new InputStreamResource(bundle.inputStream())); + } } \ No newline at end of file diff --git a/src/main/java/com/litoralregas/openvpn/ssh/SshService.java b/src/main/java/com/litoralregas/openvpn/ssh/SshService.java index 31151e2..69e9021 100644 --- a/src/main/java/com/litoralregas/openvpn/ssh/SshService.java +++ b/src/main/java/com/litoralregas/openvpn/ssh/SshService.java @@ -1,14 +1,19 @@ package com.litoralregas.openvpn.ssh; import com.jcraft.jsch.*; +import com.litoralregas.openvpn.openvpn.OpenVpnBundleDownload; import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; @Service public class SshService { private final VpsSshProperties properties; + public SshService(VpsSshProperties properties) { this.properties = properties; } @@ -23,6 +28,70 @@ public class SshService { ); } + public OpenVpnBundleDownload downloadFileFromConfiguredVps( + String remotePath, + String filename + ) { + return downloadFile( + properties.getHost(), + properties.getPort(), + properties.getUsername(), + properties.getPassword(), + remotePath, + filename + ); + } + + public OpenVpnBundleDownload downloadFile( + String host, + int port, + String username, + String password, + String remotePath, + String filename + ) { + try { + JSch jsch = new JSch(); + + Session session = jsch.getSession(username, host, port); + session.setPassword(password); + session.setConfig("StrictHostKeyChecking", "no"); + session.connect(10_000); + + ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); + channel.connect(10_000); + + SftpATTRS attrs = channel.lstat(remotePath); + long size = attrs.getSize(); + + InputStream inputStream = channel.get(remotePath); + + InputStream wrapped = new FilterInputStream(inputStream) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + channel.disconnect(); + session.disconnect(); + } + } + }; + + return new OpenVpnBundleDownload( + filename, + size, + wrapped + ); + + } catch (Exception e) { + throw new IllegalStateException( + "Failed to download file from VPS: " + remotePath, + e + ); + } + } + public SshCommandResult execute( String host, int port, diff --git a/test-router.tar.gz b/test-router.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e50ff2a018ae357eb7c0fb8c6575fb5400609bee GIT binary patch literal 5584 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 literal 0 HcmV?d00001