aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <yuuta@yuuta.moe>2021-04-02 14:12:35 -0700
committerTrumeet <yuuta@yuuta.moe>2021-04-02 14:12:35 -0700
commit26417474f09eb042b2b8547c454f572086831b84 (patch)
tree4daf6bbef613ea82f2d43232c40e54ff9524f077
parent74e4489d4736d714e22cf5c3893f1b12243a5a45 (diff)
downloaddn42peering-26417474f09eb042b2b8547c454f572086831b84.tar
dn42peering-26417474f09eb042b2b8547c454f572086831b84.tar.gz
dn42peering-26417474f09eb042b2b8547c454f572086831b84.tar.bz2
dn42peering-26417474f09eb042b2b8547c454f572086831b84.zip
feat(central): add nodes admin UI
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java19
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java72
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java142
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeHandler.java262
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java11
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java4
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/DuplicateNodeException.java4
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java12
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java92
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java9
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java39
-rw-r--r--central/src/main/resources/admin/index.ftlh29
-rw-r--r--central/src/main/resources/admin/nodes/edit.ftlh16
-rw-r--r--central/src/main/resources/admin/nodes/form.ftlh76
-rw-r--r--central/src/main/resources/admin/nodes/new.ftlh13
15 files changed, 767 insertions, 33 deletions
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
index 4844913..766aae2 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
@@ -20,15 +20,18 @@ 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.admin.nodes.NodeHandler;
import moe.yuuta.dn42peering.asn.IASNService;
import moe.yuuta.dn42peering.manage.AdminASNAuthProvider;
+import moe.yuuta.dn42peering.node.INodeService;
+import moe.yuuta.dn42peering.peer.IPeerService;
import moe.yuuta.dn42peering.portal.ISubRouter;
import javax.annotation.Nonnull;
-
import java.util.UUID;
-import static io.vertx.json.schema.common.dsl.Schemas.*;
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
public class AdminHandler implements ISubRouter {
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
@@ -37,6 +40,8 @@ public class AdminHandler implements ISubRouter {
@Override
public Router mount(@Nonnull Vertx vertx) {
final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS);
+ final INodeService nodeService = INodeService.createProxy(vertx);
+ final IPeerService peerService = IPeerService.createProxy(vertx);
final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
final Router router = Router.router(vertx);
@@ -53,7 +58,14 @@ public class AdminHandler implements ISubRouter {
.produces("text/html")
.handler(ctx -> {
final String asn = ctx.user().principal().getString("username");
- AdminUI.renderIndex(engine, asn, ctx);
+ AdminUI.renderIndex(
+ engine,
+ asnService,
+ peerService,
+ nodeService,
+ asn,
+ ctx
+ );
});
router.get("/sudo")
@@ -99,6 +111,7 @@ public class AdminHandler implements ISubRouter {
}
});
+ router.mountSubRouter("/nodes", new NodeHandler().mount(vertx));
return router;
}
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java
index 59dbe08..b97a04c 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java
@@ -1,48 +1,76 @@
package moe.yuuta.dn42peering.admin;
-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.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.common.template.TemplateEngine;
+import moe.yuuta.dn42peering.asn.IASNService;
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 moe.yuuta.dn42peering.peer.IPeerService;
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;
import static moe.yuuta.dn42peering.portal.RenderingUtils.getGeneralRenderingHandler;
class AdminUI {
public static void renderIndex(@Nonnull TemplateEngine engine,
+ @Nonnull IASNService asnService,
+ @Nonnull IPeerService peerService,
+ @Nonnull INodeService nodeService,
@Nonnull String asn,
@Nonnull RoutingContext ctx) {
- final Map<String, Object> root = new HashMap<>();
- root.put("asn", asn);
- engine.render(root, "admin/index.ftlh", getGeneralRenderingHandler(ctx));
+ Future.future(nodeService::listNodes)
+ .compose(nodes -> {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ // Don't need to do that in async thread?
+ final JsonArray mapping = new JsonArray();
+ nodes.stream().map(node -> {
+ final JsonObject nodeJson = new JsonObject();
+ nodeJson.put("id", node.getId());
+ nodeJson.put("name", node.getName());
+ nodeJson.put("publicIp", node.getPublicIp());
+ nodeJson.put("dn42IP4", node.getDn42Ip4());
+ nodeJson.put("dn42IP6", node.getDn42Ip6());
+ nodeJson.put("dn42IP6NonLL", node.getDn42Ip6NonLL());
+ nodeJson.put("internalIP", node.getInternalIp());
+ nodeJson.put("internalPort", node.getInternalPort());
+ nodeJson.put("tunnelingMethods", node.getSupportedVPNTypes());
+ return nodeJson;
+ }).forEach(mapping::add);
+ root.put("nodes", mapping);
+ return Future.succeededFuture(root);
+ })
+ .compose(root -> {
+ return Future.future(asnService::count)
+ .compose(count -> {
+ root.put("asnTotal", count);
+ return Future.succeededFuture(root);
+ });
+ })
+ .compose(root -> {
+ return Future.future(asnService::count)
+ .compose(count -> {
+ root.put("peersTotal", count);
+ return Future.succeededFuture(root);
+ });
+ })
+ .compose(json -> {
+ return engine.render(json, "admin/index.ftlh");
+ })
+ .onComplete(getGeneralRenderingHandler(ctx));
}
public static void renderSudo(@Nonnull TemplateEngine engine,
- @Nonnull String asn,
- @Nullable List<String> errors,
- @Nullable String targetASN,
- @Nonnull RoutingContext ctx) {
+ @Nonnull String asn,
+ @Nullable List<String> errors,
+ @Nullable String targetASN,
+ @Nonnull RoutingContext ctx) {
final Map<String, Object> root = new HashMap<>();
root.put("asn", asn);
root.put("errors", errors);
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java
new file mode 100644
index 0000000..dd625cc
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java
@@ -0,0 +1,142 @@
+package moe.yuuta.dn42peering.admin.nodes;
+
+import edazdarevic.commons.net.CIDRUtils;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import moe.yuuta.dn42peering.RPC;
+import moe.yuuta.dn42peering.node.INodeService;
+import moe.yuuta.dn42peering.node.Node;
+import moe.yuuta.dn42peering.portal.FormException;
+import moe.yuuta.dn42peering.portal.RenderingUtils;
+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;
+
+class NodeAdminUI {
+ public static void renderForm(@Nonnull TemplateEngine engine,
+ @Nonnull INodeService nodeService,
+ @Nonnull String asn,
+ boolean newForm,
+ @Nullable Node node,
+ @Nullable List<String> errors,
+ @Nonnull RoutingContext ctx) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("tunneling_method_wireguard", true);
+ if (node != null) {
+ root.put("id", node.getId());
+ root.put("input_asn", node.getAsn());
+ root.put("name", node.getName());
+ root.put("ipv4", node.getDn42Ip4());
+ root.put("ipv6", node.getDn42Ip6());
+ root.put("ipv6_non_ll", node.getDn42Ip6NonLL());
+ root.put("public_ip", node.getPublicIp());
+ root.put("internal_ip", node.getInternalIp());
+ root.put("internal_port", node.getInternalPort());
+ root.put("notice", node.getNotice());
+ root.put("tunneling_method_wireguard", node.isWireguard());
+ }
+ if(!newForm && node != null)
+ root.put("action", "/admin/nodes/edit?id=" + node.getId());
+ else
+ root.put("action", "/admin/nodes/new");
+ root.put("errors", errors);
+ engine.render(root, newForm ? "admin/nodes/new.ftlh" : "admin/nodes/edit.ftlh",
+ RenderingUtils.getGeneralRenderingHandler(ctx));
+ }
+
+ @Nonnull
+ public static Future<Node> parseForm(@Nonnull INodeService nodeService,
+ @Nonnull String authAsn,
+ @Nonnull JsonObject form) {
+ final List<String> errors = new ArrayList<>(10);
+ final Node node = new Node();
+ node.setAsn(form.getString("asn", authAsn));
+ node.setName(form.getString("name"));
+ node.setNotice(form.getString("notice"));
+ node.setDn42Ip4(form.getString("ipv4"));
+ node.setDn42Ip6(form.getString("ipv6"));
+ node.setDn42Ip6NonLL(form.getString("ipv6_non_ll"));
+ node.setInternalIp(form.getString("internal_ip"));
+ node.setPublicIp(form.getString("public_ip"));
+ try {
+ String raw = form.getString("internal_port");
+ if(raw == null || raw.isEmpty()) {
+ node.setInternalPort(RPC.AGENT_PORT);
+ } else {
+ node.setInternalPort(Integer.parseInt(raw));
+ }
+ } catch (NumberFormatException e) {
+ errors.add("Invalid internal port");
+ }
+ node.setWireguard(form.containsKey("tunneling_method_wireguard"));
+
+ if(!node.getAsn().matches("[aA][sS]424242[0-9][0-9][0-9][0-9]"))
+ errors.add("ASN is invalid");
+ if(node.getName() == null || node.getName().trim().isEmpty())
+ errors.add("No name supplied");
+ try {
+ if(node.getDn42Ip4() != null) {
+ if (InetAddressValidator.getInstance().isValidInet4Address(node.getDn42Ip4())) {
+ if (!new CIDRUtils("172.20.0.0/14").isInRange(node.getDn42Ip4())) {
+ errors.add("DN42 IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x).");
+ }
+ } else
+ errors.add("DN42 IPv4 address is illegal. Cannot parse your address.");
+ } else {
+ errors.add("DN42 IPv4 address is not supplied");
+ }
+ if(node.getDn42Ip6() != null) {
+ if (InetAddressValidator.getInstance().isValidInet6Address(node.getDn42Ip6())) {
+ if (new CIDRUtils("fd00::/8").isInRange(node.getDn42Ip6())) {
+ errors.add("DN42 IPv6 address is illegal. It must be a link-local IPv6 address.");
+ }
+ } else
+ errors.add("DN42 Link Local IPv6 address is illegal. Cannot parse your address.");
+ } else {
+ errors.add("DN42 IPv6 Link Local address is not supplied");
+ }
+ if(node.getDn42Ip6NonLL() != null) {
+ if (InetAddressValidator.getInstance().isValidInet6Address(node.getDn42Ip6NonLL())) {
+ if (!new CIDRUtils("fd00::/8").isInRange(node.getDn42Ip6NonLL()) ||
+ Inet6Address.getByName(node.getDn42Ip6NonLL()).isLinkLocalAddress()) {
+ errors.add("DN42 IPv6 address is illegal. It must be a dn42 IPv6 address.");
+ }
+ } else
+ errors.add("IPv6 address is illegal. Cannot parse your address.");
+ } else {
+ errors.add("DN42 IPv6 address is not supplied");
+ }
+ } catch (IOException e) {
+ return Future.failedFuture(e);
+ }
+
+ if(node.getInternalIp() == null) {
+ errors.add("Internal IP is not supplied.");
+ }
+
+ if(node.getInternalPort() < 0 ||
+ node.getInternalPort() > 65535) {
+ errors.add("Internal Port is out of range. Supported range: [0, 65535].");
+ }
+
+ if(node.getPublicIp() == null || node.getPublicIp().isEmpty()) {
+ errors.add("Public IP is not supplied.");
+ }
+
+ if(errors.isEmpty()) {
+ return Future.succeededFuture(node);
+ } else {
+ return Future.failedFuture(new FormException(node, errors.toArray(new String[]{})));
+ }
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeHandler.java b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeHandler.java
new file mode 100644
index 0000000..a9e6f5b
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeHandler.java
@@ -0,0 +1,262 @@
+package moe.yuuta.dn42peering.admin.nodes;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import io.vertx.ext.web.validation.RequestParameters;
+import io.vertx.ext.web.validation.RequestPredicate;
+import io.vertx.ext.web.validation.ValidationHandler;
+import io.vertx.ext.web.validation.builder.Bodies;
+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 io.vertx.serviceproxy.ServiceException;
+import moe.yuuta.dn42peering.asn.IASNService;
+import moe.yuuta.dn42peering.jaba.Pair;
+import moe.yuuta.dn42peering.node.DuplicateNodeException;
+import moe.yuuta.dn42peering.node.INodeService;
+import moe.yuuta.dn42peering.node.Node;
+import moe.yuuta.dn42peering.peer.IPeerService;
+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 javax.annotation.Nonnull;
+import java.util.Arrays;
+
+import static io.vertx.ext.web.validation.builder.Parameters.param;
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
+
+public class NodeHandler implements ISubRouter {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ @Nonnull
+ @Override
+ public Router mount(@Nonnull Vertx vertx) {
+ final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS);
+ final INodeService nodeService = INodeService.createProxy(vertx);
+ final IPeerService peerService = IPeerService.createProxy(vertx);
+ final IProvisionRemoteService provisionRemoteService = IProvisionRemoteService.create(vertx);
+ final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
+ final SchemaParser parser = SchemaParser.createDraft7SchemaParser(
+ SchemaRouter.create(vertx, new SchemaRouterOptions()));
+
+ final ObjectSchemaBuilder nodeSchema = objectSchema()
+ .allowAdditionalProperties(false)
+ .property("asn", stringSchema())
+ .property("name", stringSchema())
+ .property("notice", stringSchema())
+ .property("ipv4", stringSchema())
+ .property("ipv6", stringSchema())
+ .property("ipv6_non_ll", stringSchema())
+ .property("public_ip", stringSchema())
+ .property("internal_ip", stringSchema())
+ .property("internal_port", stringSchema())
+ .property("tunneling_method_wireguard", stringSchema());
+
+ final Router router = Router.router(vertx);
+ router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024));
+
+ router.get("/new")
+ .produces("text/html")
+ .handler(ctx -> NodeAdminUI.renderForm(engine,
+ nodeService,
+ ctx.user().principal().getString("username"),
+ true,
+ null,
+ null,
+ ctx));
+
+ router.post("/new")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(nodeSchema))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ NodeAdminUI.parseForm(nodeService, asn, parameters)
+ .<Node>compose(node -> Future.future(f -> nodeService.addNew(node, ar -> {
+ if (ar.succeeded()) {
+ node.setId((int) (long) ar.result());
+ f.complete(node);
+ } else {
+ if(((ServiceException)ar.cause()).getDebugInfo().getString("causeName")
+ .equals(DuplicateNodeException.class.getName())) {
+ f.fail(new FormException(node, "A node with your given public IP already exists."));
+ return;
+ }
+ f.fail(ar.cause());
+ }
+ })))
+ .onSuccess(peer -> {
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/admin")
+ .end();
+ })
+ .onFailure(err -> {
+ if (err instanceof FormException) {
+ NodeAdminUI.renderForm(engine, nodeService,
+ asn,
+ true,
+ ((Node) ((FormException) err).data),
+ Arrays.asList(((FormException) err).errors),
+ ctx);
+ } else {
+ if (!(err instanceof HTTPException)) logger.error("Cannot add node.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/edit")
+ .produces("text/html")
+ .handler(ValidationHandler
+ .builder(parser)
+ .queryParameter(param("id", stringSchema()))
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ int intId;
+ try {
+ intId = Integer.parseInt(id);
+ } catch (NumberFormatException ignored) {
+ ctx.fail(new HTTPException(400));
+ return;
+ }
+ Future.<Node>future(f -> nodeService.getNode(intId, f))
+ .compose(node -> {
+ if (node == null) {
+ return Future.failedFuture(new HTTPException(400));
+ } else {
+ return Future.succeededFuture(node);
+ }
+ })
+ .onSuccess(node ->
+ NodeAdminUI.renderForm(
+ engine,
+ nodeService,
+ asn,
+ false,
+ node,
+ null,
+ ctx))
+ .onFailure(ctx::fail);
+ });
+
+ router.post("/edit")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(nodeSchema))
+ .queryParameter(param("id", stringSchema()))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ int intId;
+ try {
+ intId = Integer.parseInt(id);
+ } catch (NumberFormatException ignored) {
+ ctx.response().setStatusCode(400).end();
+ return;
+ }
+ Future.<Node>future(f -> nodeService.getNode(intId, f))
+ .compose(node -> {
+ if(node == null) {
+ return Future.failedFuture(new HTTPException(404));
+ }
+ return Future.succeededFuture(node);
+ })
+ .compose(existingNode -> {
+ return NodeAdminUI.parseForm(nodeService, asn, parameters)
+ .compose(newNode -> {
+ newNode.setId(existingNode.getId());
+ return Future.succeededFuture(new Pair<>(existingNode, newNode));
+ });
+ })
+ .<Node>compose(node -> Future.future(f -> nodeService.updateTo(node.b, ar -> {
+ if (ar.succeeded()) {
+ f.complete(node.b);
+ } else {
+ if(((ServiceException)ar.cause()).getDebugInfo().getString("causeName")
+ .equals(DuplicateNodeException.class.getName())) {
+ f.fail(new FormException(node.b, "A node with your given public IP already exists."));
+ return;
+ }
+ f.fail(ar.cause());
+ }
+ })))
+ .onSuccess(node -> {
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/admin")
+ .end();
+ provisionRemoteService.deploy(node.getId(), ar -> {});
+ })
+ .onFailure(err -> {
+ if (err instanceof FormException) {
+ NodeAdminUI.renderForm(engine, nodeService,
+ asn,
+ true,
+ ((Node) ((FormException) err).data),
+ Arrays.asList(((FormException) err).errors),
+ ctx);
+ } else {
+ if (!(err instanceof HTTPException)) logger.error("Cannot add node.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/delete")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .queryParameter(param("id", stringSchema()))
+ .build())
+ .handler(ctx -> {
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ int intId;
+ try {
+ intId = Integer.parseInt(id);
+ } catch (NumberFormatException ignored) {
+ ctx.response().setStatusCode(400).end();
+ return;
+ }
+ nodeService.delete(intId, ar -> {
+ if(ar.succeeded()) {
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/admin")
+ .end();
+ } else {
+ logger.error("Cannot delete node " + intId, ar.cause());
+ ctx.fail(ar.cause());
+ }
+ });
+ });
+
+ return router;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
index 20cf3ce..2fce4f1 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
@@ -151,4 +151,15 @@ class ASNServiceImpl implements IASNService {
.onComplete(handler);
return this;
}
+
+ @Nonnull
+ @Override
+ public IASNService count(@Nonnull Handler<AsyncResult<Integer>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT COUNT(asn) FROM asn")
+ .execute(null)
+ .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0)))
+ .onComplete(handler);
+ return this;
+ }
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
index 5da2a57..a8f27da 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
@@ -49,4 +49,8 @@ public interface IASNService {
@Fluent
@Nonnull
IASNService lookupEmails(@Nonnull WhoisObject asn, @Nonnull Handler<AsyncResult<List<String>>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService count(@Nonnull Handler<AsyncResult<Integer>> handler);
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/DuplicateNodeException.java b/central/src/main/java/moe/yuuta/dn42peering/node/DuplicateNodeException.java
new file mode 100644
index 0000000..d32cddb
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/DuplicateNodeException.java
@@ -0,0 +1,4 @@
+package moe.yuuta.dn42peering.node;
+
+public class DuplicateNodeException extends Exception {
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java
index 9323faa..a7a5016 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java
@@ -25,4 +25,16 @@ public interface INodeService {
@Fluent
@Nonnull
INodeService getNode(int id, @Nonnull Handler<AsyncResult<Node>> handler);
+
+ @Fluent
+ @Nonnull
+ INodeService addNew(@Nonnull Node node, @Nonnull Handler<AsyncResult<Long>> handler);
+
+ @Fluent
+ @Nonnull
+ INodeService updateTo(@Nonnull Node node, @Nonnull Handler<AsyncResult<Long>> handler);
+
+ @Fluent
+ @Nonnull
+ INodeService delete(int id, @Nonnull Handler<AsyncResult<Void>> handler);
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java
index a2535d4..bc7e5bf 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java
@@ -4,15 +4,15 @@ import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
+import io.vertx.mysqlclient.MySQLException;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.SqlResult;
import io.vertx.sqlclient.templates.SqlTemplate;
import moe.yuuta.dn42peering.database.DatabaseUtils;
import javax.annotation.Nonnull;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
class NodeServiceImpl implements INodeService {
private final Pool pool;
@@ -54,7 +54,7 @@ class NodeServiceImpl implements INodeService {
.mapTo(NodeRowMapper.INSTANCE)
.execute(Collections.singletonMap("id", id))
.compose(nodeRowMappers -> {
- if(nodeRowMappers.iterator().hasNext()) {
+ if (nodeRowMappers.iterator().hasNext()) {
return Future.succeededFuture(nodeRowMappers.iterator().next());
} else {
return Future.succeededFuture(null);
@@ -63,4 +63,88 @@ class NodeServiceImpl implements INodeService {
.onComplete(handler);
return this;
}
+
+ @Nonnull
+ @Override
+ public INodeService addNew(@Nonnull Node node,
+ @Nonnull Handler<AsyncResult<Long>> handler) {
+ node.setId(0);
+ Future.<Long>future(f -> {
+ Future.<RowSet<Node>>future(f1 -> SqlTemplate
+ .forUpdate(pool, "INSERT INTO node (id, asn, name, notice, " +
+ "public_ip, " +
+ "dn42_ip4, dn42_ip6, dn42_ip6_nonll," +
+ "internal_ip, internal_port," +
+ "vpn_type_wg) " +
+ "VALUES (#{id}, #{asn}, #{name}, #{notice}, " +
+ "#{public_ip}, " +
+ "#{dn42_ip4}, #{dn42_ip6}, #{dn42_ip6_nonll}, " +
+ "#{internal_ip}, #{internal_port}, " +
+ "#{vpn_type_wg}" +
+ ")")
+ .mapFrom(NodeParametersMapper.INSTANCE)
+ .mapTo(NodeRowMapper.INSTANCE)
+ .execute(node, f1))
+ .compose(rows -> Future.succeededFuture(rows.property(DatabaseUtils.LAST_INSERTED_ID)))
+ .onFailure(err -> {
+ if (err instanceof MySQLException) {
+ if (((MySQLException) err).getErrorCode() == 1062 /* Duplicate */) {
+ f.fail(new DuplicateNodeException());
+ return;
+ }
+ f.fail(err);
+ }
+ })
+ .onSuccess(f::complete);
+ }).onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public INodeService updateTo(@Nonnull Node node, @Nonnull Handler<AsyncResult<Long>> handler) {
+ Future.<Long>future(f -> {
+ Future.<RowSet<Node>>future(f1 -> SqlTemplate
+ .forUpdate(pool, "UPDATE node SET " +
+ "asn = #{asn}," +
+ "name = #{name}," +
+ "notice = #{notice}," +
+ "public_ip = #{public_ip}," +
+ "dn42_ip4 = #{dn42_ip4}," +
+ "dn42_ip6 = #{dn42_ip6}," +
+ "dn42_ip6_nonll = #{dn42_ip6_nonll}," +
+ "internal_ip = #{internal_ip}," +
+ "internal_port = #{internal_port}," +
+ "vpn_type_wg = #{vpn_type_wg} " +
+ "WHERE id = #{id}")
+ .mapFrom(NodeParametersMapper.INSTANCE)
+ .mapTo(NodeRowMapper.INSTANCE)
+ .execute(node, f1))
+ .compose(rows -> Future.succeededFuture(rows.property(DatabaseUtils.LAST_INSERTED_ID)))
+ .onFailure(err -> {
+ if (err instanceof MySQLException) {
+ if (((MySQLException) err).getErrorCode() == 1062 /* Duplicate */) {
+ f.fail(new DuplicateNodeException());
+ return;
+ }
+ f.fail(err);
+ }
+ })
+ .onSuccess(f::complete);
+ }).onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public INodeService delete(int id, @Nonnull Handler<AsyncResult<Void>> handler) {
+ final Map<String, Object> params = new HashMap<>(2);
+ params.put("id", id);
+ Future.<SqlResult<Void>>future(f -> SqlTemplate
+ .forUpdate(pool, "DELETE FROM node WHERE id = #{id}")
+ .execute(params, f))
+ .<Void>compose(voidSqlResult -> Future.succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java
index 5f4a7af..67240e0 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java
@@ -50,4 +50,13 @@ public interface IPeerService {
@Fluent
@Nonnull
IPeerService changeProvisionStatus(int id, @Nonnull ProvisionStatus provisionStatus, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService count(@Nonnull Handler<AsyncResult<Integer>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService listUnderNode(long nodeId,
+ @Nonnull Handler<AsyncResult<List<Peer>>> handler);
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java
index 9548815..d202882 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java
@@ -139,7 +139,7 @@ class PeerServiceImpl implements IPeerService {
.execute(params, f))
.compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0)));
}
- if(futures.isEmpty()) {
+ if (futures.isEmpty()) {
Future.succeededFuture(false).onComplete(handler);
return this;
}
@@ -173,7 +173,7 @@ class PeerServiceImpl implements IPeerService {
.mapTo(PeerRowMapper.INSTANCE)
.execute(params, f))
.compose(peers -> {
- if(peers.iterator().hasNext())
+ if (peers.iterator().hasNext())
return Future.succeededFuture(peers.iterator().next());
return Future.succeededFuture(null);
})
@@ -190,8 +190,8 @@ class PeerServiceImpl implements IPeerService {
Future.<SqlResult<Void>>future(f -> SqlTemplate
.forUpdate(pool, "DELETE FROM peer WHERE id = #{id} AND asn = #{asn}")
.execute(params, f))
- .<Void>compose(voidSqlResult -> Future.succeededFuture(null))
- .onComplete(handler);
+ .<Void>compose(voidSqlResult -> Future.succeededFuture(null))
+ .onComplete(handler);
return this;
}
@@ -208,4 +208,35 @@ class PeerServiceImpl implements IPeerService {
.onComplete(handler);
return this;
}
+
+ @Nonnull
+ @Override
+ public IPeerService count(@Nonnull Handler<AsyncResult<Integer>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT COUNT(id) FROM peer")
+ .execute(null)
+ .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0)))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService listUnderNode(long nodeId, @Nonnull Handler<AsyncResult<List<Peer>>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT id, type, asn, ipv4, ipv6, " +
+ "wg_endpoint, wg_endpoint_port, " +
+ "wg_self_pubkey, wg_self_privkey, wg_peer_pubkey, wg_preshared_secret, " +
+ "provision_status, mpbgp, node FROM peer " +
+ "WHERE node = #{node}")
+ .mapTo(PeerRowMapper.INSTANCE)
+ .execute(Collections.singletonMap("node", nodeId))
+ .compose(peers -> {
+ final List<Peer> peerList = new ArrayList<>();
+ for (Peer peer : peers) peerList.add(peer);
+ return Future.succeededFuture(peerList);
+ })
+ .onComplete(handler);
+ return this;
+ }
}
diff --git a/central/src/main/resources/admin/index.ftlh b/central/src/main/resources/admin/index.ftlh
index fa6e28f..3316622 100644
--- a/central/src/main/resources/admin/index.ftlh
+++ b/central/src/main/resources/admin/index.ftlh
@@ -12,5 +12,34 @@
<ul>
<li><a href="/admin/sudo">sudo</a></li>
</ul>
+<h2>Registered ASNs and Peering</h2>
+<p>Total ASN: ${asnTotal}</p>
+<p>Total peers: ${peersTotal}</p>
+<h2>Nodes</h2>
+<a href="/admin/nodes/new">New Node</a>
+<table style="width: 100%">
+ <tr>
+ <th>Name</th>
+ <th>Public IP</th>
+ <th>DN42 IP</th>
+ <th>Internal IP</th>
+ <th>Tunnel Methods</th>
+ <th>Actions</th>
+ </tr>
+ <#list nodes as node>
+ <tr>
+ <td>${node.name}</td>
+ <td>${node.publicIp}</td>
+ <td>${node.dn42IP4}<br />${node.dn42IP6}<br />${node.dn42IP6NonLL}</td>
+ <td>${node.internalIP}:${node.internalPort?long?c}</td>
+ <td>
+ <#list node.tunnelingMethods as method>
+ ${method}<br />
+ </#list>
+ </td>
+ <td><a href="/admin/nodes/edit?id=${node.id}">Edit</a></td>
+ </tr>
+ </#list>
+</table>
</body>
</html> \ No newline at end of file
diff --git a/central/src/main/resources/admin/nodes/edit.ftlh b/central/src/main/resources/admin/nodes/edit.ftlh
new file mode 100644
index 0000000..6c0c8dc
--- /dev/null
+++ b/central/src/main/resources/admin/nodes/edit.ftlh
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <#include "/style.ftlh">
+ <title>Edit Node | Admin</title>
+</head>
+<body class="markdown-body">
+<h1>Edit Node</h1>
+<p>You are logged in as: ${asn}.</p>
+<p><b>Note: If you are switching to a new machine and had changed the internal IP, configurations on the old machine will not be removed. The new machine will be provisioned as usual.</b></p>
+<#include "form.ftlh">
+<h2>More Actions</h2>
+<a href="/admin/nodes/delete?id=${id}">Delete</a>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/admin/nodes/form.ftlh b/central/src/main/resources/admin/nodes/form.ftlh
new file mode 100644
index 0000000..43b7f3d
--- /dev/null
+++ b/central/src/main/resources/admin/nodes/form.ftlh
@@ -0,0 +1,76 @@
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="${action}" method="post">
+ <label>Basic Information</label><br />
+ <label for="asn">ASN:</label><br />
+ <input type="text" id="asn" name="asn"
+ placeholder="${asn}"
+ pattern="[aA][sS]424242[0-9][0-9][0-9][0-9]"
+ value="${(input_asn)!}"><br />
+ <br />
+ <label for="name">Name:</label><br />
+ <input type="text" id="name" name="name" required
+ placeholder="My Awesome Node"
+ value="${(name)!}"><br />
+ <br />
+ <label for="notice">Notice (Optional, HTML enabled):</label><br />
+ <input type="text" id="notice" name="notice"
+ placeholder="<b>Hi!</b>"
+ value="${(notice)!}"><br />
+ <br />
+
+ <label>DN42 Network Information</label><br />
+ <label for="ipv4">DN42 IPv4 Address:</label><br />
+ <input type="text" id="ipv4" name="ipv4" required
+ placeholder="172.22.114.10"
+ pattern="^172\.2[0-3](\.([1-9]?\d|[12]\d\d)){2}$"
+ value="${(ipv4)!}"><br />
+ <br />
+ <label for="ipv6">Link Local IPv6 Address:</label><br />
+ <input type="text" id="ipv6" name="ipv6" required
+ placeholder="fe80::2980"
+ value="${(ipv6)!}"><br />
+ <br />
+ <label for="ipv6_non_ll">DN42 IPv6 Address:</label><br />
+ <input type="text" id="ipv6_non_ll" name="ipv6_non_ll" required
+ placeholder="fd3f:a1f1:54ed::1"
+ value="${(ipv6_non_ll)!}"><br />
+ <br />
+
+ <label>Public Network Information</label><br />
+ <label for="public_ip">Public IP or Domain (No validation. For end user display only.):</label><br />
+ <input type="text" id="public_ip" name="public_ip" required
+ placeholder="tyo1.jp.dn42.yuuta.moe"
+ value="${(public_ip)!}"><br />
+ <br />
+
+ <label>Management Network Information</label><br />
+ <label for="internal_ip">Internal IP or domain (No validation. Type with care.):</label><br />
+ <input type="text" id="internal_ip" name="internal_ip" required
+ placeholder="192.168.10.1"
+ value="${(internal_ip)!}"><br />
+ <br />
+ <label for="internal_port">Internal Port:</label><br />
+ <input type="text" id="internal_port" name="internal_port"
+ placeholder="49200"
+ pattern="[0-9]+"
+ value="${(internal_port?long?c)!}"><br />
+ <br />
+
+ <label>Tunneling Methods</label><br />
+ <input type="checkbox" id="tunneling_method_wireguard"
+ name="tunneling_method_wireguard"
+ value="tunneling_method_wireguard"
+ ${tunneling_method_wireguard?string('checked', '')}>
+ <label for="tunneling_method_wireguard">Support WireGuard</label><br /><br />
+
+ <input type="submit">
+</form> \ No newline at end of file
diff --git a/central/src/main/resources/admin/nodes/new.ftlh b/central/src/main/resources/admin/nodes/new.ftlh
new file mode 100644
index 0000000..30c2524
--- /dev/null
+++ b/central/src/main/resources/admin/nodes/new.ftlh
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <#include "/style.ftlh">
+ <title>Add New Node | Admin</title>
+</head>
+<body class="markdown-body">
+<h1>Add New Node</h1>
+<p>You are logged in as: ${asn}.</p>
+<#include "form.ftlh">
+</body>
+</html> \ No newline at end of file