diff options
author | Trumeet <yuuta@yuuta.moe> | 2021-03-28 22:17:37 -0700 |
---|---|---|
committer | Trumeet <yuuta@yuuta.moe> | 2021-03-28 22:17:37 -0700 |
commit | ce7703ca44a8fb7a65cbffb7c258d043d7cd94e4 (patch) | |
tree | 62291bfa346c128f934599cb200ad0c77db30dd8 | |
parent | 53354f03d0dc8342c35073881edf78452637d68b (diff) | |
download | dn42peering-ce7703ca44a8fb7a65cbffb7c258d043d7cd94e4.tar dn42peering-ce7703ca44a8fb7a65cbffb7c258d043d7cd94e4.tar.gz dn42peering-ce7703ca44a8fb7a65cbffb7c258d043d7cd94e4.tar.bz2 dn42peering-ce7703ca44a8fb7a65cbffb7c258d043d7cd94e4.zip |
refactor(central): move some common codes outside of ManageHandler
4 files changed, 739 insertions, 721 deletions
diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java index 052f97d..b64bb13 100644 --- a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java @@ -1,14 +1,7 @@ package moe.yuuta.dn42peering.manage; -import com.wireguard.crypto.Key; -import com.wireguard.crypto.KeyFormatException; -import edazdarevic.commons.net.CIDRUtils; -import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpHeaders; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; @@ -27,32 +20,25 @@ import io.vertx.json.schema.SchemaParser; import io.vertx.json.schema.SchemaRouter; import io.vertx.json.schema.SchemaRouterOptions; import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder; -import moe.yuuta.dn42peering.provision.BGPRequestCommon; -import moe.yuuta.dn42peering.provision.WGRequestCommon; import moe.yuuta.dn42peering.asn.IASNService; import moe.yuuta.dn42peering.jaba.Pair; import moe.yuuta.dn42peering.node.INodeService; -import moe.yuuta.dn42peering.node.Node; import moe.yuuta.dn42peering.peer.IPeerService; import moe.yuuta.dn42peering.peer.Peer; -import moe.yuuta.dn42peering.peer.ProvisionStatus; import moe.yuuta.dn42peering.portal.FormException; import moe.yuuta.dn42peering.portal.HTTPException; import moe.yuuta.dn42peering.portal.ISubRouter; import moe.yuuta.dn42peering.provision.IProvisionRemoteService; import moe.yuuta.dn42peering.whois.IWhoisService; -import moe.yuuta.dn42peering.whois.WhoisObject; -import org.apache.commons.validator.routines.InetAddressValidator; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.IOException; -import java.net.Inet6Address; import java.util.*; -import java.util.stream.Collectors; import static io.vertx.ext.web.validation.builder.Parameters.param; import static io.vertx.json.schema.common.dsl.Schemas.*; +import static moe.yuuta.dn42peering.manage.ManagementUI.*; +import static moe.yuuta.dn42peering.manage.ManagementProvision.*; public class ManageHandler implements ISubRouter { private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); @@ -88,32 +74,15 @@ public class ManageHandler implements ISubRouter { final String asn = ctx.user().principal().getString("username"); Future.<List<moe.yuuta.dn42peering.peer.Peer>>future(f -> peerService.listUnderASN(asn, f)) - .<Buffer>compose(peers -> Future.future(f -> renderIndex(engine, asn, peers, f))) - .onComplete(res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - ctx.fail(res.cause()); - } - }); + .onSuccess(peers -> renderIndex(engine, asn, peers, ctx)) + .onFailure(ctx::fail); }); router.get("/new") .produces("text/html") .handler(ctx -> { final String asn = ctx.user().principal().getString("username"); - renderForm(engine, nodeService, true, asn, null, null, res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - res.cause().printStackTrace(); - ctx.fail(res.cause()); - } - }); + renderForm(engine, nodeService, true, asn, null, null, ctx); }); final ObjectSchemaBuilder registerSchema = objectSchema() @@ -147,36 +116,7 @@ public class ManageHandler implements ISubRouter { peer.setAsn(asn); return Future.succeededFuture(peer); }).compose(peer -> - Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv4(), f)) - .compose(whoisObject -> { - if (whoisObject == null || - !whoisObject.containsKey("origin") || - !whoisObject.get("origin").contains(asn)) { - return Future.failedFuture(new FormException(peer, - "The IPv4 address you specified does not have a route with your ASN.")); - } - // Verify IPv6 - try { - if (peer.getIpv6() != null && !peer.isIPv6LinkLocal()) { - return Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv6(), f)) - .compose(ipv6Whois -> { - if (ipv6Whois == null || - !ipv6Whois.containsKey("origin") || - !ipv6Whois.get("origin").contains(asn)) { - return Future.failedFuture(new FormException(peer, - "The IPv6 address you specified does not have a route with your ASN.")); - } else { - return Future.succeededFuture(peer); - } - }); - } else { - // Do not check for IPv6 address. - return Future.succeededFuture(peer); - } - } catch (IOException e) { - return Future.failedFuture(e); - } - }) + ManagementVerify.verifyAllOrThrow(whoisService, peer, asn) ).<Peer>compose(peer -> Future.future(f -> { boolean needCheckIP6 = true; try { @@ -211,7 +151,7 @@ public class ManageHandler implements ISubRouter { .putHeader("Location", "/manage") .end(); provisionPeer(nodeService, provisionService, peer).onComplete(ar -> - this.handleProvisionResult(peerService, peer, ar)); + handleProvisionResult(peerService, peer, ar)); }) .onFailure(err -> { if (err instanceof FormException) { @@ -219,15 +159,7 @@ public class ManageHandler implements ISubRouter { true, asn, ((Peer) ((FormException) err).data), Arrays.asList(((FormException) err).errors), - res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - ctx.fail(res.cause()); - } - }); + ctx); } else { if(!(err instanceof HTTPException)) logger.error("Cannot add peer.", err); ctx.fail(err); @@ -252,19 +184,9 @@ public class ManageHandler implements ISubRouter { } return Future.succeededFuture(peer); }) - .<Buffer>compose(peer -> Future.future(f -> renderForm(engine, nodeService, false, - asn, peer, null, f))) - .onComplete(res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - if(!(res.cause() instanceof HTTPException)) logger.error(String.format("Cannot show peer %s", id), - res.cause()); - ctx.fail(res.cause()); - } - }); + .onSuccess(peer -> Future.future(f -> renderForm(engine, nodeService, false, + asn, peer, null, ctx))) + .onFailure(ctx::fail); }); router.post("/edit") @@ -300,63 +222,17 @@ public class ManageHandler implements ISubRouter { }) ).compose(peer -> { final Peer inPeer = peer.b; - return Future.<WhoisObject>future(f -> whoisService.query(inPeer.getIpv4(), f)) - .compose(whoisObject -> { - if (whoisObject == null || - !whoisObject.containsKey("origin") || - !whoisObject.get("origin").contains(asn)) { - return Future.failedFuture(new FormException(inPeer, - "The IPv4 address you specified does not have a route with your ASN.")); - } - // Verify IPv6 - try { - if (inPeer.getIpv6() != null && !inPeer.isIPv6LinkLocal()) { - return Future.<WhoisObject>future(f -> whoisService.query(inPeer.getIpv6(), f)) - .compose(ipv6Whois -> { - if (ipv6Whois == null || - !ipv6Whois.containsKey("origin") || - !ipv6Whois.get("origin").contains(asn)) { - return Future.failedFuture(new FormException(inPeer, - "The IPv6 address you specified does not have a route with your ASN.")); - } else { - return Future.succeededFuture(peer); - } - }); - } else { - // Do not check for IPv6 address. - return Future.succeededFuture(peer); - } - } catch (IOException e) { - return Future.failedFuture(e); - } - }); + return ManagementVerify.verifyAllOrThrow(whoisService, inPeer, asn) + .compose(_peer -> Future.succeededFuture(peer)); } ).<Pair<Peer /* Existing */, Peer /* Input */>>compose(peer -> { final Peer existingPeer = peer.a; final Peer inPeer = peer.b; - boolean needCheckIPv4Conflict; - boolean needCheckIPv6Conflict; - - if (existingPeer.getType() != inPeer.getType()) { - needCheckIPv4Conflict = true; - needCheckIPv6Conflict = true; - } else { - needCheckIPv4Conflict = - !Objects.equals(existingPeer.getIpv4(), inPeer.getIpv4()); - needCheckIPv6Conflict = - !Objects.equals(existingPeer.getIpv6(), inPeer.getIpv6()); - if (inPeer.getIpv6() == null) needCheckIPv6Conflict = false; - else { - try { - if(inPeer.isIPv6LinkLocal()) - needCheckIPv6Conflict = false; - } catch (IOException ignored) {} - } - } - final boolean nc6 = needCheckIPv6Conflict; + final boolean needCheckIPv4Conflict = ManagementVerify.determineIfNeedCheckIPv4(existingPeer, inPeer); + final boolean needCheckIPv6Conflict = ManagementVerify.determineIfNeedCheckIPv6(existingPeer, inPeer); return Future.future(f -> peerService.isIPConflict(inPeer.getType(), needCheckIPv4Conflict ? inPeer.getIpv4() : null, - nc6 ? inPeer.getIpv6() : null, + needCheckIPv6Conflict ? inPeer.getIpv6() : null, ar -> { if (ar.succeeded()) { if (ar.result()) { @@ -383,7 +259,7 @@ public class ManageHandler implements ISubRouter { final Peer existingPeer = pair.a; final Peer inPeer = pair.b; reloadPeer(nodeService, provisionService, existingPeer, inPeer).onComplete(ar -> - this.handleProvisionResult(peerService, inPeer, ar)); + handleProvisionResult(peerService, inPeer, ar)); }) .onFailure(err -> { if (err instanceof FormException) { @@ -401,15 +277,7 @@ public class ManageHandler implements ISubRouter { asn, peer, Arrays.asList(((FormException) err).errors), - res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - ctx.fail(res.cause()); - } - }); + ctx); } else { if(!(err instanceof HTTPException)) logger.error("Cannot edit peer.", err); ctx.fail(err); @@ -449,16 +317,7 @@ public class ManageHandler implements ISubRouter { .produces("text/html") .handler(ctx -> { final String asn = ctx.user().principal().getString("username"); - renderChangepw(engine, asn, null, res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - res.cause().printStackTrace(); - ctx.fail(res.cause()); - } - }); + renderChangepw(engine, asn, null, ctx); }); router.post("/change-password") @@ -499,16 +358,7 @@ public class ManageHandler implements ISubRouter { if (err instanceof FormException) { renderChangepw(engine, asn, Arrays.asList(((FormException) err).errors), - res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - res.cause().printStackTrace(); - ctx.fail(res.cause()); - } - }); + ctx); } else { if(!(err instanceof HTTPException)) logger.error("Cannot change password.", err); ctx.fail(err); @@ -520,16 +370,7 @@ public class ManageHandler implements ISubRouter { .produces("text/html") .handler(ctx -> { final String asn = ctx.user().principal().getString("username"); - renderDA(engine, asn, null, res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - res.cause().printStackTrace(); - ctx.fail(res.cause()); - } - }); + renderDA(engine, asn, null, ctx); }); router.post("/delete-account") @@ -558,16 +399,7 @@ public class ManageHandler implements ISubRouter { if (err instanceof FormException) { renderDA(engine, asn, Arrays.asList(((FormException) err).errors), - res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - res.cause().printStackTrace(); - ctx.fail(res.cause()); - } - }); + ctx); } else { logger.error("Cannot delete account.", err); ctx.fail(err); @@ -592,539 +424,10 @@ public class ManageHandler implements ISubRouter { } return Future.succeededFuture(peer); }) - .compose(peer -> renderShowConfig(nodeService, engine, peer)) - .onComplete(res -> { - if (res.succeeded()) { - ctx.response() - .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") - .end(res.result()); - } else { - if(!(res.cause() instanceof HTTPException)) - logger.error(String.format("Cannot render show configuration %s", id), - res.cause()); - ctx.fail(res.cause()); - } - }); + .onSuccess(peer -> renderShowConfig(nodeService, engine, peer, ctx)) + .onFailure(ctx::fail); }); return router; } - - @Nonnull - private Future<Peer> parseForm(@Nonnull INodeService nodeService, - @Nonnull JsonObject form) { - // Parse form - int nodeId = -1; - if (form.containsKey("node")) { - try { - nodeId = Integer.parseInt(form.getString("node")); - } catch (NumberFormatException ignored) { - } - if(nodeId == -1) { - return Future.failedFuture(new FormException("The node selection is invalid.")); - } - } - - int n = nodeId; - return Future.<Node>future(f -> nodeService.getNode(n, f)) - .compose(node -> { - try { - final List<String> errors = new ArrayList<>(10); - if(node == null) { - errors.add("The node selection is invalid."); - } - Peer.VPNType type = null; - if (form.containsKey("vpn")) { - final String rawVPN = form.getString("vpn"); - if (rawVPN == null) { - errors.add("Tunneling type is not specified."); - } else - switch (rawVPN) { - case "wg": - type = Peer.VPNType.WIREGUARD; - break; - default: - errors.add("Tunneling type is unexpected."); - break; - } - } else { - errors.add("Tunneling type is not specified."); - } - - String ipv4 = null; - if (form.containsKey("ipv4")) { - ipv4 = form.getString("ipv4"); - if (ipv4 == null || ipv4.isEmpty()) { - errors.add("IPv4 address is not specified."); - ipv4 = null; // Non-null but empty values could cause problems. - } else { - if (InetAddressValidator.getInstance().isValidInet4Address(ipv4)) { - if (!new CIDRUtils("172.20.0.0/14").isInRange(ipv4)) { - errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); - } - } else - errors.add("IPv4 address is illegal. Cannot parse your address."); - } - } else { - errors.add("IPv4 address is not specified."); - } - - String ipv6 = null; - if (form.containsKey("ipv6")) { - ipv6 = form.getString("ipv6"); - if (ipv6 != null && !ipv6.isEmpty()) { - if (InetAddressValidator.getInstance().isValidInet6Address(ipv6)) { - if (!new CIDRUtils("fd00::/8").isInRange(ipv6) && - !Inet6Address.getByName(ipv6).isLinkLocalAddress()) { - errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address."); - } - ipv6 = ipv6.replaceAll("((?::0\\b){2,}):?(?!\\S*\\b\\1:0\\b)(\\S*)", "::$2"); - } else - errors.add("IPv6 address is illegal. Cannot parse your address."); - } else { - ipv6 = null; // Non-null but empty values could cause problems. - } - } - - boolean mpbgp = false; - if (form.containsKey("mpbgp")) { - if (ipv6 == null) { - errors.add("MP-BGP cannot be enabled if you do not have a valid IPv6 address."); - } else { - mpbgp = true; - } - } - - String wgEndpoint = null; - boolean wgEndpointCorrect = false; - if (form.containsKey("wg_endpoint")) { - if (type == Peer.VPNType.WIREGUARD) { - wgEndpoint = form.getString("wg_endpoint"); - if (wgEndpoint != null && !wgEndpoint.isEmpty()) { - if (InetAddressValidator.getInstance().isValidInet4Address(wgEndpoint)) { - if (new CIDRUtils("10.0.0.0/8").isInRange(wgEndpoint) || - new CIDRUtils("192.168.0.0/16").isInRange(wgEndpoint) || - new CIDRUtils("172.16.0.0/23").isInRange(wgEndpoint)) { - errors.add("WireGuard EndPoint is illegal. It must not be an internal address."); - } else { - wgEndpointCorrect = true; - } - } else - errors.add("WireGuard EndPoint is illegal. Cannot parse your address."); - } else { - wgEndpoint = null; // Non-null but empty values could cause problems. - } - } else { - errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); - } - } - - Integer wgEndpointPort = null; - if (form.containsKey("wg_endpoint_port")) { - if (type == Peer.VPNType.WIREGUARD) { - final String rawPort = form.getString("wg_endpoint_port"); - if(rawPort != null && !rawPort.isEmpty()) { - if (wgEndpointCorrect) { - try { - wgEndpointPort = Integer.parseInt(rawPort); - if (wgEndpointPort < 0 || wgEndpointPort > 65535) { - errors.add("WireGuard EndPoint port must be in UDP port range."); - } - } catch (NumberFormatException | NullPointerException ignored) { - errors.add("WireGuard EndPoint port is not valid. It must be a number."); - } - } else { - errors.add("WireGuard EndPoint IP is not specified or invalid, but port is specified."); - } - } - } else { - errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); - } - } - - // When user specified the endpoint without the port. - if(type == Peer.VPNType.WIREGUARD && - wgEndpointCorrect && - wgEndpointPort == null) { - errors.add("WireGuard EndPoint IP is specified, but the port is missing."); - } - - String wgPubKey = null; - if (form.containsKey("wg_pubkey")) { - if (type == Peer.VPNType.WIREGUARD) { - wgPubKey = form.getString("wg_pubkey"); - if (wgPubKey == null || wgPubKey.isEmpty()) { - errors.add("WireGuard public key is not specified."); - wgPubKey = null; // Non-null but empty values could cause problems. - } else { - try { - Key.fromBase64(wgPubKey); - } catch (KeyFormatException e) { - errors.add("WireGuard public key is not valid."); - } - } - } else { - errors.add("WireGuard tunneling is not selected but WireGuard public key appears."); - } - } else { - if (type == Peer.VPNType.WIREGUARD) { - errors.add("WireGuard public key is not specified."); - } - } - - if(node != null && !node.getSupportedVPNTypes().contains(type)) { - errors.add(String.format("Node %s does not support VPN type %s.", node.getName(), - type)); - } - - Peer peer; - if (type == Peer.VPNType.WIREGUARD) { - peer = new Peer(ipv4, ipv6, wgEndpoint, wgEndpointPort, wgPubKey, mpbgp, n); - } else { - peer = new Peer( - -1, - type, - null, /* ASN: To be filled later */ - ipv4, - ipv6, - wgEndpoint, - wgEndpointPort, - null, /* Self public key: Generate later if needed */ - null, /* Self private key: Generate later if needed */ - wgPubKey, - null /* Preshared Secret: Generate later if needed */, - ProvisionStatus.NOT_PROVISIONED, - mpbgp, - n - ); - } - if(errors.isEmpty()) { - return Future.succeededFuture(peer); - } else { - return Future.failedFuture(new FormException(peer, errors.toArray(new String[]{}))); - } - } catch (IOException e) { - return Future.failedFuture(e); - } - }); - } - - private void renderIndex(@Nonnull TemplateEngine engine, - @Nonnull String asn, @Nonnull List<Peer> peers, - @Nonnull Handler<AsyncResult<Buffer>> handler) { - final Map<String, Object> root = new HashMap<>(); - root.put("asn", asn); - root.put("peers", peers.stream() - .map(peer -> { - final Map<String, Object> map = new HashMap<>(); - map.put("id", peer.getId()); - map.put("ipv4", peer.getIpv4()); - map.put("ipv6", peer.getIpv6()); - map.put("type", peer.getType()); - map.put("provisionStatus", peer.getProvisionStatus()); - return map; - }) - .collect(Collectors.toList())); - engine.render(root, "manage/index.ftlh", handler); - } - - @SuppressWarnings("unchecked") - private void renderForm(@Nonnull TemplateEngine engine, - @Nonnull INodeService nodeService, - boolean newForm, - @Nonnull String asn, @Nullable Peer peer, @Nullable List<String> errors, - @Nonnull Handler<AsyncResult<Buffer>> handler) { - Future.future(nodeService::listNodes) - .compose(list -> { - final Map<String, Object> root = new HashMap<>(); - root.put("asn", asn); - root.put("nodes", list.stream() - .map(node -> { - final Map<String, Object> map = new HashMap<>(10); - map.put("id", node.getId()); - map.put("name", node.getName()); - map.put("public_ip", node.getPublicIp()); - map.put("notice", node.getNotice()); - map.put("asn", node.getAsn()); - map.put("vpn_types", node.getSupportedVPNTypes()); - return map; - }) - .collect(Collectors.toList())); - if (peer != null) { - root.put("ipv4", peer.getIpv4()); - root.put("ipv6", peer.getIpv6()); - switch (peer.getType()) { - case WIREGUARD: - root.put("typeWireguard", true); - break; - } - root.put("wgEndpoint", peer.getWgEndpoint()); - root.put("wgEndpointPort", peer.getWgEndpointPort()); - root.put("wgPubkey", peer.getWgPeerPubkey()); - root.put("mpbgp", peer.isMpbgp()); - root.put("node_checked", peer.getNode()); - root.put("id", peer.getId()); - } else { - root.put("typeWireguard", true); - root.put("mpbgp", false); - root.put("node_checked", ((List<Map<String, Object>>)root.get("nodes")).get(0).get("id")); - } - if(!newForm && peer != null) - root.put("action", "/manage/edit?id=" + peer.getId()); - else - root.put("action", "/manage/new"); - root.put("errors", errors); - return engine.render(root, newForm ? "manage/new.ftlh" : "manage/edit.ftlh"); - }) - .onComplete(handler); - } - - private void renderChangepw(@Nonnull TemplateEngine engine, - @Nonnull String asn, @Nullable List<String> errors, - @Nonnull Handler<AsyncResult<Buffer>> handler) { - final Map<String, Object> root = new HashMap<>(); - root.put("asn", asn); - root.put("errors", errors); - engine.render(root, "manage/changepw.ftlh", handler); - } - - private void renderDA(@Nonnull TemplateEngine engine, - @Nonnull String asn, @Nullable List<String> errors, - @Nonnull Handler<AsyncResult<Buffer>> handler) { - final Map<String, Object> root = new HashMap<>(); - root.put("asn", asn); - root.put("errors", errors); - engine.render(root, "manage/delete.ftlh", handler); - } - - @Nonnull - private Future<Buffer> renderShowConfig(@Nonnull INodeService nodeService, - @Nonnull TemplateEngine engine, - @Nonnull Peer peer) { - return Future.<Node>future(f -> nodeService.getNode(peer.getNode(), f)) - .compose(node -> { - final Map<String, Object> root = new HashMap<>(); - root.put("ipv4", peer.getIpv4()); - root.put("ipv6", peer.getIpv6()); - switch (peer.getType()) { - case WIREGUARD: - root.put("typeWireguard", true); - break; - } - root.put("wgPort", peer.calcWireGuardPort()); - root.put("wgEndpoint", peer.getWgEndpoint()); - root.put("wgEndpointPort", peer.getWgEndpointPort()); - root.put("wgPresharedSecret", peer.getWgPresharedSecret()); - root.put("wgSelfPubkey", peer.getWgSelfPubkey()); - root.put("mpbgp", peer.isMpbgp()); - root.put("peer_type", peer.getType()); - root.put("peer_ipv4", peer.getIpv4()); - root.put("peer_ipv6", peer.getIpv6()); - if(peer.getWgEndpoint() != null) { - root.put("peer_wg_listen_port", peer.getWgEndpointPort()); - } - - if(node == null) { - root.put("ipv4", "This node is currently down! Edit the peer to choose another one."); - root.put("ipv6", "This node is currently down! Edit the peer to choose another one."); - root.put("asn", "This node is currently down! Edit the peer to choose another one."); - root.put("endpoint", "This node is currently down! Edit the peer to choose another one."); - root.put("show_example_config", false); - } else { - root.put("show_example_config", true); - root.put("ipv4", node.getDn42Ip4()); - try { - if(peer.isIPv6LinkLocal()) { - root.put("ipv6", node.getDn42Ip6()); - root.put("peer_link_local", true); - } else { - root.put("ipv6", node.getDn42Ip6NonLL()); - root.put("peer_link_local", false); - } - } catch (IOException e) { - return Future.failedFuture(e); - } - root.put("asn", node.getAsn().substring(2)); - root.put("endpoint", node.getPublicIp()); - } - - return engine.render(root, "manage/showconf.ftlh"); - }); - - } - - @Nonnull - private Future<Void> reloadPeer(@Nonnull INodeService nodeService, - @Nonnull IProvisionRemoteService provisionService, - @Nonnull Peer existingPeer, @Nonnull Peer inPeer) { - // Check if we can reload on the fly. - // Otherwise, we can only deprovision and provision. - // This will cause unnecessary wastes. - boolean canReload = inPeer.getType() == existingPeer.getType() && - inPeer.getNode() == existingPeer.getNode(); - // wg-quick does not support switching local IP addresses. - // However, switch between link local addresses and real IPv6 addresses require the change of - // local v6 address. Therefore, in such cases, we have to do a full re-provision. - if(canReload && // Only check if no other factors prevent us from reloading. - inPeer.getType() == Peer.VPNType.WIREGUARD && - existingPeer.getType() == Peer.VPNType.WIREGUARD) { - try { - final boolean existingLL = existingPeer.isIPv6LinkLocal(); - final boolean newLL = inPeer.isIPv6LinkLocal(); - if(existingLL != newLL) { - canReload = false; - } - } catch (IOException e) { - return Future.failedFuture(e); - } - } - // wg-quick will also not clear EndPoint setting if we just reload it. - if(canReload && // Only check if no other factors prevent us from reloading. - inPeer.getType() == Peer.VPNType.WIREGUARD && - existingPeer.getType() == Peer.VPNType.WIREGUARD) { - if(inPeer.getWgEndpoint() == null && - existingPeer.getWgEndpoint() != null) { - canReload = false; - } - } - Future<Void> future; - if (canReload) { - future = Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f)) - .compose(node -> { - if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) { - return Future.failedFuture("The node does not exist"); - } - return Future.succeededFuture(node); - }) - .compose(node -> { - switch (existingPeer.getType()) { - case WIREGUARD: - final WGRequestCommon wgReq = inPeer.toWGRequest(); - wgReq.setNode(node.toRPCNode()); - return Future.<String>future(f -> provisionService.reloadWG( - node.toRPCNode(), - wgReq, - f) - ).compose(device -> Future.succeededFuture(new Pair<>(node, device))); - default: - throw new UnsupportedOperationException("Bug: Unknown type."); - } - }) - .compose(pair -> { - final BGPRequestCommon bgpReq = inPeer.toBGPRequest(); - bgpReq.setNode(pair.a.toRPCNode()); - bgpReq.setDevice(pair.b); - return Future.future(f -> provisionService.reloadBGP( - pair.a.toRPCNode(), - bgpReq, - f)); - }); - } else { - future = unprovisionPeer(nodeService, provisionService, existingPeer) - .compose(f -> provisionPeer(nodeService, provisionService, inPeer)); - } - return future; - } - - private Future<Void> unprovisionPeer(@Nonnull INodeService nodeService, - @Nonnull IProvisionRemoteService provisionService, - @Nonnull Peer existingPeer) { - return Future.<Node>future(f -> nodeService.getNode(existingPeer.getNode(), f)) - .compose(node -> { - if(node == null) { - return Future.failedFuture("The node does not exist"); - } - return Future.succeededFuture(node); - }) - .compose(node -> { - switch (existingPeer.getType()) { - case WIREGUARD: - return Future.<Void>future(f -> provisionService.deleteWG( - node.toRPCNode(), - new WGRequestCommon(null, - (long)existingPeer.getId(), - null, - null, - null, - null, - null, - null, - null), - f)) - .compose(res -> Future.succeededFuture(node)); - default: - throw new UnsupportedOperationException("Bug: Unknown type."); - } - }) - .compose(node -> { - return Future.future(f -> provisionService.deleteBGP( - node.toRPCNode(), - new BGPRequestCommon(null, - (long)existingPeer.getId(), - null, - null, - null, - null, - null), - f)); - }) - ; - } - - @Nonnull - private Future<Void> provisionPeer(@Nonnull INodeService nodeService, - @Nonnull IProvisionRemoteService provisionService, - @Nonnull Peer inPeer) { - return Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f)) - .compose(node -> { - if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) { - return Future.failedFuture("The node does not exist"); - } - return Future.succeededFuture(node); - }) - .compose(node -> { - switch (inPeer.getType()) { - case WIREGUARD: - final WGRequestCommon wgReq = inPeer.toWGRequest(); - wgReq.setNode(node.toRPCNode()); - return Future.<String>future(f -> provisionService.provisionWG( - node.toRPCNode(), - wgReq, - f) - ).compose(device -> Future.succeededFuture(new Pair<>(node, device))); - default: - throw new UnsupportedOperationException("Bug: Unknown type."); - } - }) - .compose(pair -> { - final BGPRequestCommon bgpReq = inPeer.toBGPRequest(); - bgpReq.setNode(pair.a.toRPCNode()); - bgpReq.setDevice(pair.b); - return Future.future(f -> provisionService.provisionBGP( - pair.a.toRPCNode(), - bgpReq, - f)); - }); - } - - private void handleProvisionResult(@Nonnull IPeerService peerService, - @Nonnull Peer inPeer, - @Nonnull AsyncResult<Void> res) { - if(res.succeeded()) { - peerService.changeProvisionStatus(inPeer.getId(), - ProvisionStatus.PROVISIONED, ar -> { - if (ar.failed()) { - logger.error(String.format("Cannot update %d to provisioned.", inPeer.getId()), ar.cause()); - } - }); - } else { - logger.error(String.format("Cannot provision %d.", inPeer.getId()), res.cause()); - peerService.changeProvisionStatus(inPeer.getId(), - ProvisionStatus.FAIL, ar -> { - if (ar.failed()) { - logger.error(String.format("Cannot update %d to failed.", inPeer.getId()), ar.cause()); - } - }); - } - } } diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementProvision.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementProvision.java new file mode 100644 index 0000000..5c9b531 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementProvision.java @@ -0,0 +1,197 @@ +package moe.yuuta.dn42peering.manage; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import moe.yuuta.dn42peering.jaba.Pair; +import moe.yuuta.dn42peering.node.INodeService; +import moe.yuuta.dn42peering.node.Node; +import moe.yuuta.dn42peering.peer.IPeerService; +import moe.yuuta.dn42peering.peer.Peer; +import moe.yuuta.dn42peering.peer.ProvisionStatus; +import moe.yuuta.dn42peering.provision.BGPRequestCommon; +import moe.yuuta.dn42peering.provision.IProvisionRemoteService; +import moe.yuuta.dn42peering.provision.WGRequestCommon; + +import javax.annotation.Nonnull; +import java.io.IOException; + +class ManagementProvision { + private static final Logger logger = LoggerFactory.getLogger(ManagementProvision.class.getSimpleName()); + + @Nonnull + public static Future<Void> reloadPeer(@Nonnull INodeService nodeService, + @Nonnull IProvisionRemoteService provisionService, + @Nonnull Peer existingPeer, @Nonnull Peer inPeer) { + // Check if we can reload on the fly. + // Otherwise, we can only deprovision and provision. + // This will cause unnecessary wastes. + boolean canReload = inPeer.getType() == existingPeer.getType() && + inPeer.getNode() == existingPeer.getNode(); + // wg-quick does not support switching local IP addresses. + // However, switch between link local addresses and real IPv6 addresses require the change of + // local v6 address. Therefore, in such cases, we have to do a full re-provision. + if(canReload && // Only check if no other factors prevent us from reloading. + inPeer.getType() == Peer.VPNType.WIREGUARD && + existingPeer.getType() == Peer.VPNType.WIREGUARD) { + try { + final boolean existingLL = existingPeer.isIPv6LinkLocal(); + final boolean newLL = inPeer.isIPv6LinkLocal(); + if(existingLL != newLL) { + canReload = false; + } + } catch (IOException e) { + return Future.failedFuture(e); + } + } + // wg-quick will also not clear EndPoint setting if we just reload it. + if(canReload && // Only check if no other factors prevent us from reloading. + inPeer.getType() == Peer.VPNType.WIREGUARD && + existingPeer.getType() == Peer.VPNType.WIREGUARD) { + if(inPeer.getWgEndpoint() == null && + existingPeer.getWgEndpoint() != null) { + canReload = false; + } + } + Future<Void> future; + if (canReload) { + future = Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f)) + .compose(node -> { + if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) { + return Future.failedFuture("The node does not exist"); + } + return Future.succeededFuture(node); + }) + .compose(node -> { + switch (existingPeer.getType()) { + case WIREGUARD: + final WGRequestCommon wgReq = inPeer.toWGRequest(); + wgReq.setNode(node.toRPCNode()); + return Future.<String>future(f -> provisionService.reloadWG( + node.toRPCNode(), + wgReq, + f) + ).compose(device -> Future.succeededFuture(new Pair<>(node, device))); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(pair -> { + final BGPRequestCommon bgpReq = inPeer.toBGPRequest(); + bgpReq.setNode(pair.a.toRPCNode()); + bgpReq.setDevice(pair.b); + return Future.future(f -> provisionService.reloadBGP( + pair.a.toRPCNode(), + bgpReq, + f)); + }); + } else { + future = unprovisionPeer(nodeService, provisionService, existingPeer) + .compose(f -> provisionPeer(nodeService, provisionService, inPeer)); + } + return future; + } + + public static Future<Void> unprovisionPeer(@Nonnull INodeService nodeService, + @Nonnull IProvisionRemoteService provisionService, + @Nonnull Peer existingPeer) { + return Future.<Node>future(f -> nodeService.getNode(existingPeer.getNode(), f)) + .compose(node -> { + if(node == null) { + return Future.failedFuture("The node does not exist"); + } + return Future.succeededFuture(node); + }) + .compose(node -> { + switch (existingPeer.getType()) { + case WIREGUARD: + return Future.<Void>future(f -> provisionService.deleteWG( + node.toRPCNode(), + new WGRequestCommon(null, + (long)existingPeer.getId(), + null, + null, + null, + null, + null, + null, + null), + f)) + .compose(res -> Future.succeededFuture(node)); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(node -> { + return Future.future(f -> provisionService.deleteBGP( + node.toRPCNode(), + new BGPRequestCommon(null, + (long)existingPeer.getId(), + null, + null, + null, + null, + null), + f)); + }) + ; + } + + @Nonnull + public static Future<Void> provisionPeer(@Nonnull INodeService nodeService, + @Nonnull IProvisionRemoteService provisionService, + @Nonnull Peer inPeer) { + return Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f)) + .compose(node -> { + if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) { + return Future.failedFuture("The node does not exist"); + } + return Future.succeededFuture(node); + }) + .compose(node -> { + switch (inPeer.getType()) { + case WIREGUARD: + final WGRequestCommon wgReq = inPeer.toWGRequest(); + wgReq.setNode(node.toRPCNode()); + return Future.<String>future(f -> provisionService.provisionWG( + node.toRPCNode(), + wgReq, + f) + ).compose(device -> Future.succeededFuture(new Pair<>(node, device))); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(pair -> { + final BGPRequestCommon bgpReq = inPeer.toBGPRequest(); + bgpReq.setNode(pair.a.toRPCNode()); + bgpReq.setDevice(pair.b); + return Future.future(f -> provisionService.provisionBGP( + pair.a.toRPCNode(), + bgpReq, + f)); + }); + } + + public static void handleProvisionResult(@Nonnull IPeerService peerService, + @Nonnull Peer inPeer, + @Nonnull AsyncResult<Void> res) { + if(res.succeeded()) { + peerService.changeProvisionStatus(inPeer.getId(), + ProvisionStatus.PROVISIONED, ar -> { + if (ar.failed()) { + logger.error(String.format("Cannot update %d to provisioned.", inPeer.getId()), ar.cause()); + } + }); + } else { + logger.error(String.format("Cannot provision %d.", inPeer.getId()), res.cause()); + peerService.changeProvisionStatus(inPeer.getId(), + ProvisionStatus.FAIL, ar -> { + if (ar.failed()) { + logger.error(String.format("Cannot update %d to failed.", inPeer.getId()), ar.cause()); + } + }); + } + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java new file mode 100644 index 0000000..641df19 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java @@ -0,0 +1,420 @@ +package moe.yuuta.dn42peering.manage; + +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; +import edazdarevic.commons.net.CIDRUtils; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.common.template.TemplateEngine; +import moe.yuuta.dn42peering.node.INodeService; +import moe.yuuta.dn42peering.node.Node; +import moe.yuuta.dn42peering.peer.Peer; +import moe.yuuta.dn42peering.peer.ProvisionStatus; +import moe.yuuta.dn42peering.portal.FormException; +import org.apache.commons.validator.routines.InetAddressValidator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.Inet6Address; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class ManagementUI { + private static Handler<AsyncResult<Buffer>> getGeneralRenderingHandler(@Nonnull RoutingContext ctx) { + return res -> { + if (res.succeeded()) { + ctx.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "text/html") + .end(res.result()); + } else { + ctx.fail(res.cause()); + } + }; + } + + public static void renderIndex(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nonnull List<Peer> peers, + @Nonnull RoutingContext ctx) { + renderIndex(engine, asn, peers, getGeneralRenderingHandler(ctx)); + } + + public static void renderIndex(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nonnull List<Peer> peers, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + root.put("peers", peers.stream() + .map(peer -> { + final Map<String, Object> map = new HashMap<>(); + map.put("id", peer.getId()); + map.put("ipv4", peer.getIpv4()); + map.put("ipv6", peer.getIpv6()); + map.put("type", peer.getType()); + map.put("provisionStatus", peer.getProvisionStatus()); + return map; + }) + .collect(Collectors.toList())); + engine.render(root, "manage/index.ftlh", handler); + } + + public static void renderForm(@Nonnull TemplateEngine engine, + @Nonnull INodeService nodeService, + boolean newForm, + @Nonnull String asn, @Nullable Peer peer, @Nullable List<String> errors, + @Nonnull RoutingContext ctx) { + renderForm(engine, nodeService, newForm, asn, peer, errors, getGeneralRenderingHandler(ctx)); + } + + @SuppressWarnings("unchecked") + public static void renderForm(@Nonnull TemplateEngine engine, + @Nonnull INodeService nodeService, + boolean newForm, + @Nonnull String asn, @Nullable Peer peer, @Nullable List<String> errors, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + Future.future(nodeService::listNodes) + .compose(list -> { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + root.put("nodes", list.stream() + .map(node -> { + final Map<String, Object> map = new HashMap<>(10); + map.put("id", node.getId()); + map.put("name", node.getName()); + map.put("public_ip", node.getPublicIp()); + map.put("notice", node.getNotice()); + map.put("asn", node.getAsn()); + map.put("vpn_types", node.getSupportedVPNTypes()); + return map; + }) + .collect(Collectors.toList())); + if (peer != null) { + root.put("ipv4", peer.getIpv4()); + root.put("ipv6", peer.getIpv6()); + switch (peer.getType()) { + case WIREGUARD: + root.put("typeWireguard", true); + break; + } + root.put("wgEndpoint", peer.getWgEndpoint()); + root.put("wgEndpointPort", peer.getWgEndpointPort()); + root.put("wgPubkey", peer.getWgPeerPubkey()); + root.put("mpbgp", peer.isMpbgp()); + root.put("node_checked", peer.getNode()); + root.put("id", peer.getId()); + } else { + root.put("typeWireguard", true); + root.put("mpbgp", false); + root.put("node_checked", ((List<Map<String, Object>>)root.get("nodes")).get(0).get("id")); + } + if(!newForm && peer != null) + root.put("action", "/manage/edit?id=" + peer.getId()); + else + root.put("action", "/manage/new"); + root.put("errors", errors); + return engine.render(root, newForm ? "manage/new.ftlh" : "manage/edit.ftlh"); + }) + .onComplete(handler); + } + + + public static void renderChangepw(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nullable List<String> errors, + @Nonnull RoutingContext ctx) { + renderChangepw(engine, asn, errors, getGeneralRenderingHandler(ctx)); + } + + public static void renderChangepw(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nullable List<String> errors, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + root.put("errors", errors); + engine.render(root, "manage/changepw.ftlh", handler); + } + + public static void renderDA(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nullable List<String> errors, + @Nonnull RoutingContext ctx) { + renderDA(engine, asn, errors, getGeneralRenderingHandler(ctx)); + } + + public static void renderDA(@Nonnull TemplateEngine engine, + @Nonnull String asn, @Nullable List<String> errors, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + root.put("errors", errors); + engine.render(root, "manage/delete.ftlh", handler); + } + + public static void renderShowConfig(@Nonnull INodeService nodeService, + @Nonnull TemplateEngine engine, + @Nonnull Peer peer, + @Nonnull RoutingContext ctx) { + renderShowConfig(nodeService, engine, peer).onComplete(getGeneralRenderingHandler(ctx)); + } + + @Nonnull + public static Future<Buffer> renderShowConfig(@Nonnull INodeService nodeService, + @Nonnull TemplateEngine engine, + @Nonnull Peer peer) { + return Future.<Node>future(f -> nodeService.getNode(peer.getNode(), f)) + .compose(node -> { + final Map<String, Object> root = new HashMap<>(); + root.put("ipv4", peer.getIpv4()); + root.put("ipv6", peer.getIpv6()); + switch (peer.getType()) { + case WIREGUARD: + root.put("typeWireguard", true); + break; + } + root.put("wgPort", peer.calcWireGuardPort()); + root.put("wgEndpoint", peer.getWgEndpoint()); + root.put("wgEndpointPort", peer.getWgEndpointPort()); + root.put("wgPresharedSecret", peer.getWgPresharedSecret()); + root.put("wgSelfPubkey", peer.getWgSelfPubkey()); + root.put("mpbgp", peer.isMpbgp()); + root.put("peer_type", peer.getType()); + root.put("peer_ipv4", peer.getIpv4()); + root.put("peer_ipv6", peer.getIpv6()); + if(peer.getWgEndpoint() != null) { + root.put("peer_wg_listen_port", peer.getWgEndpointPort()); + } + + if(node == null) { + root.put("ipv4", "This node is currently down! Edit the peer to choose another one."); + root.put("ipv6", "This node is currently down! Edit the peer to choose another one."); + root.put("asn", "This node is currently down! Edit the peer to choose another one."); + root.put("endpoint", "This node is currently down! Edit the peer to choose another one."); + root.put("show_example_config", false); + } else { + root.put("show_example_config", true); + root.put("ipv4", node.getDn42Ip4()); + try { + if(peer.isIPv6LinkLocal()) { + root.put("ipv6", node.getDn42Ip6()); + root.put("peer_link_local", true); + } else { + root.put("ipv6", node.getDn42Ip6NonLL()); + root.put("peer_link_local", false); + } + } catch (IOException e) { + return Future.failedFuture(e); + } + root.put("asn", node.getAsn().substring(2)); + root.put("endpoint", node.getPublicIp()); + } + + return engine.render(root, "manage/showconf.ftlh"); + }); + + } + + @Nonnull + public static Future<Peer> parseForm(@Nonnull INodeService nodeService, + @Nonnull JsonObject form) { + // Parse form + int nodeId = -1; + if (form.containsKey("node")) { + try { + nodeId = Integer.parseInt(form.getString("node")); + } catch (NumberFormatException ignored) { + } + if(nodeId == -1) { + return Future.failedFuture(new FormException("The node selection is invalid.")); + } + } + + int n = nodeId; + return Future.<Node>future(f -> nodeService.getNode(n, f)) + .compose(node -> { + try { + final List<String> errors = new ArrayList<>(10); + if(node == null) { + errors.add("The node selection is invalid."); + } + Peer.VPNType type = null; + if (form.containsKey("vpn")) { + final String rawVPN = form.getString("vpn"); + if (rawVPN == null) { + errors.add("Tunneling type is not specified."); + } else + switch (rawVPN) { + case "wg": + type = Peer.VPNType.WIREGUARD; + break; + default: + errors.add("Tunneling type is unexpected."); + break; + } + } else { + errors.add("Tunneling type is not specified."); + } + + String ipv4 = null; + if (form.containsKey("ipv4")) { + ipv4 = form.getString("ipv4"); + if (ipv4 == null || ipv4.isEmpty()) { + errors.add("IPv4 address is not specified."); + ipv4 = null; // Non-null but empty values could cause problems. + } else { + if (InetAddressValidator.getInstance().isValidInet4Address(ipv4)) { + if (!new CIDRUtils("172.20.0.0/14").isInRange(ipv4)) { + errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); + } + } else + errors.add("IPv4 address is illegal. Cannot parse your address."); + } + } else { + errors.add("IPv4 address is not specified."); + } + + String ipv6 = null; + if (form.containsKey("ipv6")) { + ipv6 = form.getString("ipv6"); + if (ipv6 != null && !ipv6.isEmpty()) { + if (InetAddressValidator.getInstance().isValidInet6Address(ipv6)) { + if (!new CIDRUtils("fd00::/8").isInRange(ipv6) && + !Inet6Address.getByName(ipv6).isLinkLocalAddress()) { + errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address."); + } + ipv6 = ipv6.replaceAll("((?::0\\b){2,}):?(?!\\S*\\b\\1:0\\b)(\\S*)", "::$2"); + } else + errors.add("IPv6 address is illegal. Cannot parse your address."); + } else { + ipv6 = null; // Non-null but empty values could cause problems. + } + } + + boolean mpbgp = false; + if (form.containsKey("mpbgp")) { + if (ipv6 == null) { + errors.add("MP-BGP cannot be enabled if you do not have a valid IPv6 address."); + } else { + mpbgp = true; + } + } + + String wgEndpoint = null; + boolean wgEndpointCorrect = false; + if (form.containsKey("wg_endpoint")) { + if (type == Peer.VPNType.WIREGUARD) { + wgEndpoint = form.getString("wg_endpoint"); + if (wgEndpoint != null && !wgEndpoint.isEmpty()) { + if (InetAddressValidator.getInstance().isValidInet4Address(wgEndpoint)) { + if (new CIDRUtils("10.0.0.0/8").isInRange(wgEndpoint) || + new CIDRUtils("192.168.0.0/16").isInRange(wgEndpoint) || + new CIDRUtils("172.16.0.0/23").isInRange(wgEndpoint)) { + errors.add("WireGuard EndPoint is illegal. It must not be an internal address."); + } else { + wgEndpointCorrect = true; + } + } else + errors.add("WireGuard EndPoint is illegal. Cannot parse your address."); + } else { + wgEndpoint = null; // Non-null but empty values could cause problems. + } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + } + } + + Integer wgEndpointPort = null; + if (form.containsKey("wg_endpoint_port")) { + if (type == Peer.VPNType.WIREGUARD) { + final String rawPort = form.getString("wg_endpoint_port"); + if(rawPort != null && !rawPort.isEmpty()) { + if (wgEndpointCorrect) { + try { + wgEndpointPort = Integer.parseInt(rawPort); + if (wgEndpointPort < 0 || wgEndpointPort > 65535) { + errors.add("WireGuard EndPoint port must be in UDP port range."); + } + } catch (NumberFormatException | NullPointerException ignored) { + errors.add("WireGuard EndPoint port is not valid. It must be a number."); + } + } else { + errors.add("WireGuard EndPoint IP is not specified or invalid, but port is specified."); + } + } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + } + } + + // When user specified the endpoint without the port. + if(type == Peer.VPNType.WIREGUARD && + wgEndpointCorrect && + wgEndpointPort == null) { + errors.add("WireGuard EndPoint IP is specified, but the port is missing."); + } + + String wgPubKey = null; + if (form.containsKey("wg_pubkey")) { + if (type == Peer.VPNType.WIREGUARD) { + wgPubKey = form.getString("wg_pubkey"); + if (wgPubKey == null || wgPubKey.isEmpty()) { + errors.add("WireGuard public key is not specified."); + wgPubKey = null; // Non-null but empty values could cause problems. + } else { + try { + Key.fromBase64(wgPubKey); + } catch (KeyFormatException e) { + errors.add("WireGuard public key is not valid."); + } + } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard public key appears."); + } + } else { + if (type == Peer.VPNType.WIREGUARD) { + errors.add("WireGuard public key is not specified."); + } + } + + if(node != null && !node.getSupportedVPNTypes().contains(type)) { + errors.add(String.format("Node %s does not support VPN type %s.", node.getName(), + type)); + } + + Peer peer; + if (type == Peer.VPNType.WIREGUARD) { + peer = new Peer(ipv4, ipv6, wgEndpoint, wgEndpointPort, wgPubKey, mpbgp, n); + } else { + peer = new Peer( + -1, + type, + null, /* ASN: To be filled later */ + ipv4, + ipv6, + wgEndpoint, + wgEndpointPort, + null, /* Self public key: Generate later if needed */ + null, /* Self private key: Generate later if needed */ + wgPubKey, + null /* Preshared Secret: Generate later if needed */, + ProvisionStatus.NOT_PROVISIONED, + mpbgp, + n + ); + } + if(errors.isEmpty()) { + return Future.succeededFuture(peer); + } else { + return Future.failedFuture(new FormException(peer, errors.toArray(new String[]{}))); + } + } catch (IOException e) { + return Future.failedFuture(e); + } + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementVerify.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementVerify.java new file mode 100644 index 0000000..28de0d9 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementVerify.java @@ -0,0 +1,98 @@ +package moe.yuuta.dn42peering.manage; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import moe.yuuta.dn42peering.peer.Peer; +import moe.yuuta.dn42peering.portal.FormException; +import moe.yuuta.dn42peering.whois.IWhoisService; +import moe.yuuta.dn42peering.whois.WhoisObject; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Objects; + +class ManagementVerify { + public static Future<Peer> verifyIPv4RouteOrThrow(@Nonnull IWhoisService whoisService, + @Nonnull Peer peer, + @Nonnull String asn) { + return Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv4(), f)) + .compose(whoisObject -> { + if (whoisObject == null || + !whoisObject.containsKey("origin") || + !whoisObject.get("origin").contains(asn)) { + return Future.failedFuture(new FormException(peer, + "The IPv4 address you specified does not have a route with your ASN.")); + } else { + return Future.succeededFuture(peer); + } + }); + } + + public static Future<Peer> verifyIPv6RouteOrThrow(@Nonnull IWhoisService whoisService, + @Nonnull Peer peer, + @Nonnull String asn) { + try { + if (peer.getIpv6() != null && !peer.isIPv6LinkLocal()) { + return Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv6(), f)) + .compose(ipv6Whois -> { + if (ipv6Whois == null || + !ipv6Whois.containsKey("origin") || + !ipv6Whois.get("origin").contains(asn)) { + return Future.failedFuture(new FormException(peer, + "The IPv6 address you specified does not have a route with your ASN.")); + } else { + return Future.succeededFuture(peer); + } + }); + } else { + // Do not check for IPv6 address. + return Future.succeededFuture(peer); + } + } catch (IOException e) { + return Future.failedFuture(e); + } + } + + public static Future<Peer> verifyAllOrThrow(@Nonnull IWhoisService whoisService, + @Nonnull Peer peer, + @Nonnull String asn) { + return CompositeFuture.all(verifyIPv4RouteOrThrow(whoisService, peer, asn), + verifyIPv6RouteOrThrow(whoisService, peer, asn)) + .compose(_res -> Future.succeededFuture(peer)); + } + + public static boolean determineIfNeedCheckIPv4(@Nonnull Peer existingPeer, + @Nonnull Peer inPeer) { + boolean needCheckIPv4Conflict; + + if (existingPeer.getType() != inPeer.getType()) { + needCheckIPv4Conflict = true; + } else { + needCheckIPv4Conflict = + !Objects.equals(existingPeer.getIpv4(), inPeer.getIpv4()); + } + + return needCheckIPv4Conflict; + } + + public static boolean determineIfNeedCheckIPv6(@Nonnull Peer existingPeer, + @Nonnull Peer inPeer) { + boolean needCheckIPv6Conflict; + + if (existingPeer.getType() != inPeer.getType()) { + needCheckIPv6Conflict = true; + } else { + needCheckIPv6Conflict = + !Objects.equals(existingPeer.getIpv6(), inPeer.getIpv6()); + if (inPeer.getIpv6() == null) needCheckIPv6Conflict = false; + else { + try { + if(inPeer.isIPv6LinkLocal()) + needCheckIPv6Conflict = false; + } catch (IOException ignored) {} + } + } + + return needCheckIPv6Conflict; + } +} |