Ready for production adds missing endpoints

This commit is contained in:
litoral05
2026-05-18 12:28:27 +01:00
parent 72f7805085
commit 811b163e5a
15 changed files with 572 additions and 6 deletions
@@ -1,7 +1,6 @@
package com.litoralregas.vpnorchestrator.vps; package com.litoralregas.vpnorchestrator.vps;
import com.litoralregas.vpnorchestrator.vps.dto.NetworkTrafficResponse; import com.litoralregas.vpnorchestrator.vps.dto.*;
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -37,4 +36,78 @@ public class VpsController {
public NetworkTrafficResponse getUdp2rawTraffic() { public NetworkTrafficResponse getUdp2rawTraffic() {
return wireGuardService.getUdp2rawTraffic(); return wireGuardService.getUdp2rawTraffic();
} }
@GetMapping(
value = "/controllers/clients",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ControllerClientsResponse getControllerClients() {
return wireGuardService.getControllerClients();
}
@GetMapping("/controllers/clients/{clientId}")
public ControllerClientFileResponse getControllerClientFile(
@PathVariable String clientId
) {
return wireGuardService.getControllerClientFile(clientId);
}
@PostMapping("/controllers/clients/{clientId}")
public ControllerClientFileResponse updateControllerClientFile(
@PathVariable String clientId,
@RequestBody ControllerClientFileUpdateRequest request
) {
return wireGuardService.updateControllerClientFile(
clientId,
request.content()
);
}
@PostMapping("/controllers/clients")
public ControllerClientFileResponse createControllerClient(
@RequestBody ControllerClientCreateRequest request
) {
return wireGuardService.createControllerClient(
request.name(),
request.password()
);
}
@DeleteMapping("controllers/clients")
public ControllerClientFileResponse deleteControllerClient(
@RequestBody DeleteControllerClientRequest request
) {
return wireGuardService.deleteControllerClient(request.clientId());
}
@PostMapping("/controllers/routers/firewall/rules")
public RouterFirewallRulesResponse getRouterFirewallRules(
@RequestBody RouterFirewallRulesRequest request
) {
return wireGuardService.getRouterFirewallRules(
request.routerHost(),
request.routerPassword()
);
}
@PostMapping("/controllers/routers/firewall/rules/create")
public RouterFirewallRuleWriteResponse createRouterFirewallRule(
@RequestBody RouterFirewallRuleWriteRequest request
) {
return wireGuardService.createRouterFirewallRule(request);
}
@PostMapping("/controllers/routers/firewall/rules/update")
public RouterFirewallRuleWriteResponse updateRouterFirewallRule(
@RequestBody RouterFirewallRuleWriteRequest request
) {
return wireGuardService.updateRouterFirewallRule(request);
}
@DeleteMapping("/controllers/routers/firewall/rules")
public RouterFirewallRuleWriteResponse deleteRouterFirewallRule(
@RequestBody RouterFirewallRuleDeleteRequest request
) {
return wireGuardService.deleteRouterFirewallRule(request);
}
} }
@@ -2,12 +2,10 @@ package com.litoralregas.vpnorchestrator.vps;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.litoralregas.vpnorchestrator.vps.dto.NetworkTrafficResponse; import com.litoralregas.vpnorchestrator.vps.dto.*;
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashSet; import java.util.*;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -217,4 +215,395 @@ public class WireGuardService {
); );
} }
} }
public ControllerClientsResponse getControllerClients() {
SshCommandResult result = sshService.executeOnConfiguredVps(
"sudo /usr/local/sbin/lr-controllers-list"
);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to list controller clients: " + result.stderr()
);
}
try {
return objectMapper.readValue(
result.stdout(),
ControllerClientsResponse.class
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid controller clients JSON returned by script",
e
);
}
}
public ControllerClientFileResponse getControllerClientFile(String clientId) {
String safeClientId = validateControllerClientId(clientId);
SshCommandResult result = sshService.executeOnConfiguredVps(
"sudo /usr/local/sbin/lr-controller-client-read " + shellQuote(safeClientId)
);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to read controller client file: " + result.stderr()
);
}
try {
return objectMapper.readValue(
result.stdout(),
ControllerClientFileResponse.class
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid controller client file JSON returned by script",
e
);
}
}
private String validateControllerClientId(String clientId) {
if (clientId == null || !clientId.matches("^[A-Za-z0-9_-]+$")) {
throw new IllegalArgumentException("Invalid controller client id");
}
return clientId;
}
public ControllerClientFileResponse updateControllerClientFile(
String clientId,
String content
) {
String safeClientId = validateControllerClientId(clientId);
if (content == null || content.isBlank()) {
throw new IllegalArgumentException("Content is required");
}
if (content.contains("\0")) {
throw new IllegalArgumentException("Content contains invalid characters");
}
String command = "sudo /usr/local/sbin/lr-controller-client-write "
+ shellQuote(safeClientId) + " "
+ shellQuote(content);
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to update controller client file: "
+ result.stderr()
+ result.stdout()
);
}
try {
return objectMapper.readValue(
result.stdout(),
ControllerClientFileResponse.class
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid controller client write JSON returned by script",
e
);
}
}
public ControllerClientFileResponse createControllerClient(
String name,
String password
) {
String safeName = validateNewControllerClientName(name);
String safePassword = validateControllerClientPassword(password);
String command = "sudo /usr/local/sbin/lr-controller-client-create "
+ shellQuote(safeName) + " "
+ shellQuote(safePassword);
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to create controller client file: " + result.stderr()
);
}
try {
return objectMapper.readValue(
result.stdout(),
ControllerClientFileResponse.class
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid controller client create JSON returned by script",
e
);
}
}
private String validateNewControllerClientName(String name) {
if (name == null) {
throw new IllegalArgumentException("Client name is required");
}
String trimmed = name.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("Client name is required");
}
if (!trimmed.matches("^[A-Za-z0-9 _-]+$")) {
throw new IllegalArgumentException(
"Client name can only contain letters, numbers, spaces, underscores and hyphens"
);
}
return trimmed;
}
private String validateControllerClientPassword(String password) {
if (password == null || password.trim().isEmpty()) {
throw new IllegalArgumentException("Client password is required");
}
String trimmed = password.trim();
if (trimmed.contains("xxx") || trimmed.contains("\n") || trimmed.contains("\r")) {
throw new IllegalArgumentException("Client password contains invalid characters");
}
return trimmed;
}
public ControllerClientFileResponse deleteControllerClient(String clientId) {
if (clientId.isEmpty()) throw new IllegalArgumentException("Client id is required");
String command = "sudo /usr/local/sbin/lr-controller-client-delete "
+ shellQuote(clientId);
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to delete controller client file: " + result.stderr()
);
}
try {
return objectMapper.readValue(
result.stdout(),
ControllerClientFileResponse.class
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid controller client delete JSON returned by script",
e
);
}
}
public RouterFirewallRulesResponse getRouterFirewallRules(
String routerHost,
String routerPassword
) {
String command = "sudo /usr/local/sbin/lr-router-firewall-show "
+ shellQuote(routerHost) + " "
+ shellQuote(routerPassword);
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to read router firewall rules: " + result.stderr()
);
}
try {
RawRouterFirewallResponse raw = objectMapper.readValue(
result.stdout(),
RawRouterFirewallResponse.class
);
return new RouterFirewallRulesResponse(
raw.ok(),
raw.routerHost(),
parseFirewallRules(raw.firewall()),
java.time.Instant.now().toString()
);
} catch (JsonProcessingException e) {
throw new IllegalStateException(
"Invalid router firewall rules JSON returned by script",
e
);
}
}
private List<RouterDnatRule> parseFirewallRules(String firewall) {
if (firewall == null || firewall.isBlank()) {
return List.of();
}
Map<String, Map<String, String>> sections = new LinkedHashMap<>();
for (String line : firewall.split("\n")) {
line = line.trim();
if (line.isBlank()) continue;
int equalsIndex = line.indexOf('=');
if (equalsIndex < 0) continue;
String left = line.substring(0, equalsIndex);
String value = line.substring(equalsIndex + 1)
.replace("'", "");
int dotIndex = left.indexOf('.');
if (dotIndex < 0) continue;
String remainder = left.substring(dotIndex + 1);
int fieldIndex = remainder.indexOf('.');
String sectionId;
String field;
if (fieldIndex < 0) {
sectionId = remainder;
field = "__type";
} else {
sectionId = remainder.substring(0, fieldIndex);
field = remainder.substring(fieldIndex + 1);
}
sections
.computeIfAbsent(sectionId, ignored -> new LinkedHashMap<>())
.put(field, value);
}
return sections.entrySet().stream()
.map(entry -> {
String id = entry.getKey();
Map<String, String> fields = entry.getValue();
if (!"redirect".equals(fields.get("__type"))) {
return null;
}
if (!"DNAT".equals(fields.get("target"))) {
return null;
}
return new RouterDnatRule(
id,
fields.get("name"),
fields.get("proto"),
fields.get("src"),
fields.get("dest"),
fields.get("src_dport"),
fields.get("dest_ip"),
fields.get("dest_port"),
fields.get("target")
);
})
.filter(Objects::nonNull)
.toList();
}
public RouterFirewallRuleWriteResponse createRouterFirewallRule(
RouterFirewallRuleWriteRequest request
) {
return runRouterFirewallWriteScript(
"/usr/local/sbin/lr-router-firewall-create",
request
);
}
public RouterFirewallRuleWriteResponse updateRouterFirewallRule(
RouterFirewallRuleWriteRequest request
) {
return runRouterFirewallWriteScript(
"/usr/local/sbin/lr-router-firewall-update",
request
);
}
public RouterFirewallRuleWriteResponse deleteRouterFirewallRule(
RouterFirewallRuleDeleteRequest request
) {
String command = "sudo /usr/local/sbin/lr-router-firewall-delete "
+ shellQuote(request.routerHost()) + " "
+ shellQuote(request.routerPassword()) + " "
+ shellQuote(request.id());
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to delete router firewall rule: "
+ result.stderr()
+ result.stdout()
);
}
return new RouterFirewallRuleWriteResponse(
true,
request.routerHost(),
null,
request.id(),
java.time.Instant.now().toString()
);
}
private RouterFirewallRuleWriteResponse runRouterFirewallWriteScript(
String scriptPath,
RouterFirewallRuleWriteRequest request
) {
String command = "sudo " + scriptPath + " "
+ shellQuote(request.routerHost()) + " "
+ shellQuote(request.routerPassword()) + " "
+ shellQuote(request.id()) + " "
+ shellQuote(request.name()) + " "
+ shellQuote(request.proto()) + " "
+ shellQuote(request.srcZone()) + " "
+ shellQuote(request.destZone()) + " "
+ shellQuote(request.externalPort()) + " "
+ shellQuote(request.internalIp()) + " "
+ shellQuote(request.internalPort());
SshCommandResult result = sshService.executeOnConfiguredVps(command);
if (result.exitCode() != 0) {
throw new SshCommandException(
"Failed to write router firewall rule: "
+ result.stderr()
+ result.stdout()
);
}
return new RouterFirewallRuleWriteResponse(
true,
request.routerHost(),
new RouterDnatRule(
request.id(),
request.name(),
request.proto(),
request.srcZone(),
request.destZone(),
request.externalPort(),
request.internalIp(),
request.internalPort(),
"DNAT"
),
null,
java.time.Instant.now().toString()
);
}
} }
@@ -0,0 +1,6 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record ControllerClientCreateRequest(
String name,
String password
) {}
@@ -0,0 +1,5 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record ControllerClientFileResponse(
String content
) {}
@@ -0,0 +1,5 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record ControllerClientFileUpdateRequest(
String content
) {}
@@ -0,0 +1,10 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record ControllerClientSummary(
String id,
String name,
String file,
String path,
Integer controller_count,
String modified_at
) {}
@@ -0,0 +1,7 @@
package com.litoralregas.vpnorchestrator.vps.dto;
import java.util.List;
public record ControllerClientsResponse(
List<ControllerClientSummary> clients
) {}
@@ -0,0 +1,5 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record DeleteControllerClientRequest(
String clientId
) {}
@@ -0,0 +1,7 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RawRouterFirewallResponse(
boolean ok,
String routerHost,
String firewall
) {}
@@ -0,0 +1,13 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RouterDnatRule(
String id,
String name,
String proto,
String srcZone,
String destZone,
String externalPort,
String internalIp,
String internalPort,
String target
) {}
@@ -0,0 +1,7 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RouterFirewallRuleDeleteRequest(
String routerHost,
String routerPassword,
String id
) {}
@@ -0,0 +1,14 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RouterFirewallRuleWriteRequest(
String routerHost,
String routerPassword,
String id,
String name,
String proto,
String srcZone,
String destZone,
String externalPort,
String internalIp,
String internalPort
) {}
@@ -0,0 +1,9 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RouterFirewallRuleWriteResponse(
boolean ok,
String routerHost,
RouterDnatRule rule,
String deletedId,
String fetchedAt
) {}
@@ -0,0 +1,6 @@
package com.litoralregas.vpnorchestrator.vps.dto;
public record RouterFirewallRulesRequest(
String routerHost,
String routerPassword
) {}
@@ -0,0 +1,10 @@
package com.litoralregas.vpnorchestrator.vps.dto;
import java.util.List;
public record RouterFirewallRulesResponse(
boolean ok,
String routerHost,
List<RouterDnatRule> rules,
String fetchedAt
) {}