Ready for production adds missing endpoints
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package com.litoralregas.vpnorchestrator.vps;
|
||||
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.NetworkTrafficResponse;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.*;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -37,4 +36,78 @@ public class VpsController {
|
||||
public NetworkTrafficResponse 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.databind.ObjectMapper;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.NetworkTrafficResponse;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.VpsHealthResponse;
|
||||
import com.litoralregas.vpnorchestrator.vps.dto.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.litoralregas.vpnorchestrator.vps.dto;
|
||||
|
||||
public record ControllerClientCreateRequest(
|
||||
String name,
|
||||
String password
|
||||
) {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.litoralregas.vpnorchestrator.vps.dto;
|
||||
|
||||
public record ControllerClientFileResponse(
|
||||
String content
|
||||
) {}
|
||||
+5
@@ -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
|
||||
) {}
|
||||
+5
@@ -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
|
||||
) {}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.litoralregas.vpnorchestrator.vps.dto;
|
||||
|
||||
public record RouterFirewallRuleDeleteRequest(
|
||||
String routerHost,
|
||||
String routerPassword,
|
||||
String id
|
||||
) {}
|
||||
+14
@@ -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
|
||||
) {}
|
||||
+9
@@ -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
|
||||
) {}
|
||||
+10
@@ -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
|
||||
) {}
|
||||
Reference in New Issue
Block a user