diff options
author | Trumeet <17158086+Trumeet@users.noreply.github.com> | 2021-01-07 19:45:29 -0800 |
---|---|---|
committer | Trumeet <17158086+Trumeet@users.noreply.github.com> | 2021-01-07 19:45:29 -0800 |
commit | a5ed3da568f7542daa5e49ca0c1505f8c27d658c (patch) | |
tree | 80282452da704245e8a9a509f21b9f3d7a515ecb | |
download | dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar.gz dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar.bz2 dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.zip |
First Commit
89 files changed, 6193 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2c5c1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..2a44701 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <annotationProcessing> + <profile name="Gradle Imported" enabled="true"> + <outputRelativeToContentRoot value="true" /> + <processorPath useClasspath="false"> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-codegen/4.0.1-SNAPSHOT/4edc23f1ebaeabf5fa00ec6ca159c52c3e4f969d/vertx-codegen-4.0.1-SNAPSHOT-processor.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-service-proxy/4.0.1-SNAPSHOT/4a538246ab4622cf9df586e7fb2b9146676fe93f/vertx-service-proxy-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-core/4.0.1-SNAPSHOT/970f49c0b49b63f98126c85edfb4c1d09d87c1af/vertx-core-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.11.3/c2351800432bdbdd8284c3f5a7f0782a352aa84a/jackson-core-2.11.3.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.49.Final/6a2064cc62c7d18719742e1e101199c04c66356c/netty-handler-proxy-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.49.Final/ca35293757f80cd2460c80791757db261615dbe7/netty-codec-http2-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.49.Final/4f30dbc462b26c588dffc0eb7552caef1a0f549e/netty-codec-http-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.49.Final/c73443adb9d085d5dc2d5b7f3bdd91d5963976f7/netty-handler-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.49.Final/281770b3bac1c54f7ac995e79d953b5a66094acb/netty-resolver-dns-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.49.Final/df75527823f9fd13f6bd9d9098bd9eb786dcafb5/netty-codec-socks-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.49.Final/7ed5faaea7ad4428902844151d7006121bf367a3/netty-codec-dns-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.49.Final/20218de83c906348283f548c255650fd06030424/netty-codec-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.49.Final/415ea7f326635743aec952fe2349ca45959e94a7/netty-transport-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.49.Final/8e819a81bca88d1e88137336f64531a53db0a4ad/netty-buffer-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.49.Final/eb81e1f0eaa99e75983bf3d28cae2b103e0f3a34/netty-resolver-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.49.Final/927c8563a1662d869b145e70ce82ad89100f2c90/netty-common-4.1.49.Final.jar" /> + </processorPath> + <module name="dn42peering.agent.main" /> + </profile> + <profile name="Gradle Imported" enabled="true"> + <outputRelativeToContentRoot value="true" /> + <processorPath useClasspath="false"> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-sql-client-templates/4.0.1-SNAPSHOT/4959aaa959f2d450e362b69eaea9a384d002d1ff/vertx-sql-client-templates-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-codegen/4.0.1-SNAPSHOT/4edc23f1ebaeabf5fa00ec6ca159c52c3e4f969d/vertx-codegen-4.0.1-SNAPSHOT-processor.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-service-proxy/4.0.1-SNAPSHOT/4a538246ab4622cf9df586e7fb2b9146676fe93f/vertx-service-proxy-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-sql-client/4.0.1-SNAPSHOT/f845d420c5a6e3b1ad6964fb0b19619d0ab39299/vertx-sql-client-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-core/4.0.1-SNAPSHOT/970f49c0b49b63f98126c85edfb4c1d09d87c1af/vertx-core-4.0.1-SNAPSHOT.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.11.3/c2351800432bdbdd8284c3f5a7f0782a352aa84a/jackson-core-2.11.3.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.49.Final/6a2064cc62c7d18719742e1e101199c04c66356c/netty-handler-proxy-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.49.Final/ca35293757f80cd2460c80791757db261615dbe7/netty-codec-http2-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.49.Final/4f30dbc462b26c588dffc0eb7552caef1a0f549e/netty-codec-http-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.49.Final/c73443adb9d085d5dc2d5b7f3bdd91d5963976f7/netty-handler-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.49.Final/281770b3bac1c54f7ac995e79d953b5a66094acb/netty-resolver-dns-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.49.Final/df75527823f9fd13f6bd9d9098bd9eb786dcafb5/netty-codec-socks-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.49.Final/7ed5faaea7ad4428902844151d7006121bf367a3/netty-codec-dns-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.49.Final/20218de83c906348283f548c255650fd06030424/netty-codec-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.49.Final/415ea7f326635743aec952fe2349ca45959e94a7/netty-transport-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.49.Final/8e819a81bca88d1e88137336f64531a53db0a4ad/netty-buffer-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.49.Final/eb81e1f0eaa99e75983bf3d28cae2b103e0f3a34/netty-resolver-4.1.49.Final.jar" /> + <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.49.Final/927c8563a1662d869b145e70ce82ad89100f2c90/netty-common-4.1.49.Final.jar" /> + </processorPath> + <module name="dn42peering.central.main" /> + </profile> + </annotationProcessing> + <bytecodeTargetLevel target="1.8" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..97f4d0c --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RemoteRepositoriesConfiguration"> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="Maven Central repository" /> + <option name="url" value="https://repo1.maven.org/maven2" /> + </remote-repository> + <remote-repository> + <option name="id" value="jboss.community" /> + <option name="name" value="JBoss Community repository" /> + <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> + </remote-repository> + <remote-repository> + <option name="id" value="MavenRepo" /> + <option name="name" value="MavenRepo" /> + <option name="url" value="https://repo.maven.apache.org/maven2/" /> + </remote-repository> + <remote-repository> + <option name="id" value="BintrayJCenter" /> + <option name="name" value="BintrayJCenter" /> + <option name="url" value="https://jcenter.bintray.com/" /> + </remote-repository> + <remote-repository> + <option name="id" value="maven" /> + <option name="name" value="maven" /> + <option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" /> + </remote-repository> + </component> +</project>
\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ddaffea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ExternalStorageConfigurationManager" enabled="true" /> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="11" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/modules/dn42peering.iml b/.idea/modules/dn42peering.iml new file mode 100644 index 0000000..bfdf8a5 --- /dev/null +++ b/.idea/modules/dn42peering.iml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.linked.project.id="dn42peering" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="moe.yuuta" external.system.module.version="1.0" type="JAVA_MODULE" version="4" />
\ No newline at end of file diff --git a/.idea/modules/dn42peering.main.iml b/.idea/modules/dn42peering.main.iml new file mode 100644 index 0000000..3f1a522 --- /dev/null +++ b/.idea/modules/dn42peering.main.iml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.system.module.type="sourceSet" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager"> + <output url="file://$MODULE_DIR$/../../build/classes/java/main" /> + <exclude-output /> + <content url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main"> + <sourceFolder url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" /> + </content> + <content url="file://$MODULE_DIR$/../../central/src/main"> + <sourceFolder url="file://$MODULE_DIR$/../../central/src/main/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/../../central/src/main/resources" type="java-resource" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="library" scope="PROVIDED" name="Gradle: com.google.code.findbugs:jsr305:3.0.2" level="project" /> + <orderEntry type="library" name="Gradle: commons-validator:commons-validator:1.7" level="project" /> + <orderEntry type="library" name="Gradle: commons-logging:commons-logging:1.2" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-service-proxy:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-auth-common:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-buffer:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-core:2.11.3" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-bridge-common:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: commons-beanutils:commons-beanutils:1.9.4" level="project" /> + <orderEntry type="library" name="Gradle: org.apache.commons:commons-lang3:3.11" level="project" /> + <orderEntry type="library" scope="PROVIDED" name="Gradle: io.vertx:vertx-codegen:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-mail-client:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-codec-http:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-sql-client:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: commons-digester:commons-digester:2.1" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-web-validation:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-web:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="Gradle: org.apache.commons:commons-text:1.9" level="project" /> + <orderEntry type="library" name="Gradle: commons-net:commons-net:3.7.2" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-web-templ-freemarker:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-mysql-client:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-sql-client-templates:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-core:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: commons-collections:commons-collections:3.2.2" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-json-schema:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: io.vertx:vertx-web-common:4.0.1-SNAPSHOT" level="project" /> + <orderEntry type="library" name="Gradle: org.freemarker:freemarker:2.3.29" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-handler-proxy:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-codec-http2:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-handler:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-resolver-dns:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-transport:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-resolver:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-common:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-codec-socks:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-codec:4.1.49.Final" level="project" /> + <orderEntry type="library" name="Gradle: io.netty:netty-codec-dns:4.1.49.Final" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/.idea/runConfigurations/Agent.xml b/.idea/runConfigurations/Agent.xml new file mode 100644 index 0000000..c9e8255 --- /dev/null +++ b/.idea/runConfigurations/Agent.xml @@ -0,0 +1,23 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Agent" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$/agent" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value=":run" /> + </list> + </option> + <option name="vmOptions" value="" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <method v="2" /> + </configuration> +</component>
\ No newline at end of file diff --git a/.idea/runConfigurations/Central.xml b/.idea/runConfigurations/Central.xml new file mode 100644 index 0000000..410a176 --- /dev/null +++ b/.idea/runConfigurations/Central.xml @@ -0,0 +1,28 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Central" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="env"> + <map> + <entry key="VERTXWEB_ENVIRONMENT" value="dev" /> + </map> + </option> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$/central" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value=":run" /> + </list> + </option> + <option name="vmOptions" value="" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <method v="2" /> + </configuration> +</component>
\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..19620bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# Docker image for central +FROM docker.io/openjdk:8-jdk AS builder + +ADD . /root/ +WORKDIR /root/ +RUN ./gradlew :central:installDist + +FROM docker.io/openjdk:8-jre-alpine AS runtime + +WORKDIR / +COPY --from=0 /root/central/build/install/ ./usr/ + +ENTRYPOINT [ '/usr/bin/central' ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..df20f10 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# dn42peering + +A dn42 auto-peering platform. + +## Components + +* **Central**: Installs on some machines. They will provide HTTP portal and store user data. +* **Agent**: Installs on target nodes. They will receive commands from the **central** and apply changes to the system. + +Refer to project Wiki for more details. + +## Overview + +The central provides a HTTP portal, and connects to a MySQL database. It will perform most tasks, including user management and peering management. + +The central establishes connections to agents when necessary. It will ask the agents to perform provisioning tasks. For example, setup BGP and VPN tunnels. + +The whole project is written in Java, with the support of Vert.x framework. The communication between centrals and agents is done by gRPC. + +## Get Started + +See [Quick Start](docs/QuickStart.md) for more details. + +## License + +Proprietary Software with open source components.
\ No newline at end of file diff --git a/agent/build.gradle b/agent/build.gradle new file mode 100644 index 0000000..570c479 --- /dev/null +++ b/agent/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'application' + id "com.google.protobuf" version "0.8.14" + id 'idea' +} + +group 'moe.yuuta' +version '1.0' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +application { + getMainClass().set('moe.yuuta.dn42peering.agent.Main') +} + +dependencies { + compileOnly 'com.google.code.findbugs:jsr305:3.0.2' + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' + + implementation 'org.apache.commons:commons-text:1.9' + implementation 'commons-net:commons-net:3.7.2' + + implementation 'commons-validator:commons-validator:1.7' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + + implementation "io.vertx:vertx-core:${project.vertxVersion}" + implementation "io.vertx:vertx-web-templ-freemarker:${project.vertxVersion}" + implementation "io.vertx:vertx-service-proxy:${project.vertxVersion}" + implementation "io.vertx:vertx-grpc:${project.vertxVersion}" + annotationProcessor "io.vertx:vertx-codegen:${project.vertxVersion}:processor" + annotationProcessor "io.vertx:vertx-service-proxy:${project.vertxVersion}" + compileOnly "io.vertx:vertx-codegen:${project.vertxVersion}" + + implementation project(':rpc-common') +} + +test { + useJUnitPlatform() +}
\ No newline at end of file diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java new file mode 100644 index 0000000..33a65fb --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java @@ -0,0 +1,27 @@ +package moe.yuuta.dn42peering.agent; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; + +public class IOUtils { + @Nonnull + public static String read(@Nonnull InputStream in) throws IOException { + StringBuilder data = new StringBuilder(); + while(true) { + int i = in.read(); + if(i == -1) break; + data.append((char)i); + } + in.close(); + return data.toString(); + } + + @Nonnull + public static String readFromResource(@Nonnull ClassLoader cl, @Nonnull String name) throws IOException { + final InputStream in = cl.getResourceAsStream(name); + final String data = read(in); + in.close(); + return data; + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java new file mode 100644 index 0000000..f46d8d0 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java @@ -0,0 +1,45 @@ +package moe.yuuta.dn42peering.agent; + +import io.vertx.core.*; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import moe.yuuta.dn42peering.agent.grpc.RPCVerticle; +import moe.yuuta.dn42peering.agent.provision.ProvisionVerticle; + +import javax.annotation.Nonnull; +import java.io.FileInputStream; +import java.io.InputStream; + +public class Main { + public static void main(@Nonnull String... args) throws Throwable { + if(args.length != 1) { + System.err.println("Usage: central <path/to/config.json>"); + System.exit(64); + return; + } + + System.setProperty("vertx.logger-delegate-factory-class-name", + "io.vertx.core.logging.JULLogDelegateFactory"); + + final InputStream in = new FileInputStream(args[0]); + final JsonObject config = new JsonObject(IOUtils.read(in)); + in.close(); + + final Vertx vertx = Vertx.vertx(new VertxOptions()); + final DeploymentOptions options = new DeploymentOptions() + .setConfig(config) + .setInstances(Runtime.getRuntime().availableProcessors() * 2); + Logger logger = LoggerFactory.getLogger("Main"); + CompositeFuture.all( + Future.<String>future(f -> vertx.deployVerticle(ProvisionVerticle.class.getName(), options, f)), + Future.<String>future(f -> vertx.deployVerticle(RPCVerticle.class.getName(), options, f)) + ).onComplete(res -> { + if (res.succeeded()) { + logger.info("The server started."); + } else { + logger.error("Cannot deploy the server.", res.cause()); + } + }); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java new file mode 100644 index 0000000..565def5 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java @@ -0,0 +1,112 @@ +package moe.yuuta.dn42peering.agent.grpc; + +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 moe.yuuta.dn42peering.agent.proto.*; +import moe.yuuta.dn42peering.agent.provision.IProvisionService; + +import javax.annotation.Nonnull; + +class AgentServiceImpl extends VertxAgentGrpc.AgentVertxImplBase { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final Vertx vertx; + private final IProvisionService provisionService; + + AgentServiceImpl(@Nonnull Vertx vertx) { + this.vertx = vertx; + this.provisionService = IProvisionService.create(vertx); + } + + @Override + public Future<BGPReply> provisionBGP(BGPRequest request) { + return Future.<Void>future(f -> provisionService.provisionBGP( + request.getNode().getIpv4(), + request.getNode().getIpv6(), + (int)request.getId(), + request.getIpv4(), + request.getIpv6().isEmpty() ? null : request.getIpv6(), + request.getDevice(), + request.getMpbgp(), + request.getAsn(), + f)) + .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build())) + .onFailure(err -> logger.error(String.format("Cannot provision BGP for %d", request.getId()), + err)); + } + + @Override + public Future<BGPReply> reloadBGP(BGPRequest request) { + return Future.<Void>future(f -> provisionService.reloadBGP( + request.getNode().getIpv4(), + request.getNode().getIpv6(), + (int)request.getId(), + request.getIpv4(), + request.getIpv6().isEmpty() ? null : request.getIpv6(), + request.getDevice(), + request.getMpbgp(), + request.getAsn(), + f)) + .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build())) + .onFailure(err -> logger.error(String.format("Cannot reload BGP for %d", request.getId()), + err)); + } + + @Override + public Future<BGPReply> deleteBGP(BGPRequest request) { + return Future.<Void>future(f -> provisionService.unprovisionBGP((int)request.getId(), f)) + .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build())) + .onFailure(err -> logger.error(String.format("Cannot delete BGP for %d", request.getId()), + err)); + } + + @Override + public Future<WGReply> provisionWG(WGRequest request) { + return Future.<String>future(f -> provisionService.provisionVPNWireGuard( + request.getNode().getIpv4(), + request.getNode().getIpv6(), + (int)request.getId(), + request.getListenPort(), + request.getEndpoint(), + request.getPeerPubKey(), + request.getSelfPrivKey(), + request.getSelfPresharedSecret(), + request.getPeerIPv4(), + request.getPeerIPv6().isEmpty() ? null : request.getPeerIPv6(), + f)) + .compose(dev -> Future.succeededFuture(WGReply.newBuilder() + .setDevice(dev).build())) + .onFailure(err -> logger.error(String.format("Cannot provision WireGuard for %d", request.getId()), + err)); + } + + @Override + public Future<WGReply> reloadWG(WGRequest request) { + return Future.<String>future(f -> provisionService.reloadVPNWireGuard( + request.getNode().getIpv4(), + request.getNode().getIpv6(), + (int)request.getId(), + request.getListenPort(), + request.getEndpoint(), + request.getPeerPubKey(), + request.getSelfPrivKey(), + request.getSelfPresharedSecret(), + request.getPeerIPv4(), + request.getPeerIPv6().isEmpty() ? null : request.getPeerIPv6(), + f)) + .compose(dev -> Future.succeededFuture(WGReply.newBuilder() + .setDevice(dev).build())) + .onFailure(err -> logger.error(String.format("Cannot reload WireGuard for %d", request.getId()), + err)); + } + + @Override + public Future<WGReply> deleteWG(WGRequest request) { + return Future.<Void>future(f -> provisionService.unprovisionVPNWireGuard((int)request.getId(), f)) + .compose(_v -> Future.succeededFuture(WGReply.newBuilder().build())) + .onFailure(err -> logger.error(String.format("Cannot delete WireGuard for %d", request.getId()), + err)); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java new file mode 100644 index 0000000..3390b47 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java @@ -0,0 +1,26 @@ +package moe.yuuta.dn42peering.agent.grpc; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +import io.vertx.grpc.VertxServer; +import io.vertx.grpc.VertxServerBuilder; +import moe.yuuta.dn42peering.RPC; + +public class RPCVerticle extends AbstractVerticle { + private VertxServer server; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + server = VertxServerBuilder + .forAddress(vertx, vertx.getOrCreateContext().config().getString("internal_ip"), + RPC.AGENT_PORT) + .addService(new AgentServiceImpl(vertx)) + .build() + .start(startPromise); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + server.shutdown(stopPromise); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java new file mode 100644 index 0000000..f6bfd64 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java @@ -0,0 +1,48 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +import javax.annotation.Nonnull; +import java.io.IOException; + +public class AsyncShell { + @Nonnull + public static Future<Integer> exec(@Nonnull Vertx vertx, + @Nonnull String... cmd) { + return vertx.executeBlocking(f -> { + try { + int res = new ProcessBuilder() + .command(cmd) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor(); + f.complete(res); + } catch (IOException | InterruptedException e) { + f.fail(e); + } + }); + } + + public static void exec(@Nonnull Vertx vertx, + @Nonnull Handler<AsyncResult<Integer>> handler, + @Nonnull String... cmd) { + exec(vertx, cmd).onComplete(handler); + } + + @Nonnull + public static Future<Void> execSucc(@Nonnull Vertx vertx, + @Nonnull String... cmd) { + return Future.future(f -> exec(vertx, cmd) + .onSuccess(res -> { + if(res != 0) { + f.fail(String.format("Unexpected return code %d", res)); + } else { + f.complete(null); + } + }) + .onFailure(f::fail)); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java new file mode 100644 index 0000000..07083e7 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java @@ -0,0 +1,80 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.ProxyGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +@ProxyGen +public interface IProvisionService { + String ADDRESS = IProvisionService.class.getName(); + + @Nonnull + static IProvisionService create(@Nonnull Vertx vertx) { + return new IProvisionServiceVertxEBProxy(vertx, ADDRESS); + } + + @Fluent + @Nonnull + IProvisionService provisionBGP(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + @Nonnull String ipv4, + @Nullable String ipv6, + @Nullable String device, + boolean mpbgp, + @Nonnull String asn, + @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IProvisionService reloadBGP(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + @Nonnull String ipv4, + @Nullable String ipv6, + @Nullable String device, + boolean mpbgp, + @Nonnull String asn, + @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IProvisionService unprovisionBGP(int id, @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IProvisionService provisionVPNWireGuard(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + int listenPort, + @Nonnull String endpointWithPort, + @Nonnull String peerPubKey, + @Nonnull String selfPrivKey, + @Nonnull String selfPresharedSecret, + @Nonnull String peerIPv4, + @Nullable String peerIPv6, + @Nonnull Handler<AsyncResult<String>> handler); + + @Fluent + @Nonnull + IProvisionService reloadVPNWireGuard(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + int listenPort, + @Nonnull String endpointWithPort, + @Nonnull String peerPubKey, + @Nonnull String selfPrivKey, + @Nonnull String selfPresharedSecret, + @Nonnull String peerIPv4, + @Nullable String peerIPv6, + @Nonnull Handler<AsyncResult<String>> handler); + + @Fluent + @Nonnull + IProvisionService unprovisionVPNWireGuard(int id, @Nonnull Handler<AsyncResult<Void>> handler); +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java new file mode 100644 index 0000000..fa672c8 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java @@ -0,0 +1,286 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.file.AsyncFile; +import io.vertx.core.file.OpenOptions; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.ext.web.common.template.TemplateEngine; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.Inet6Address; +import java.util.HashMap; +import java.util.Map; + +class ProvisionServiceImpl implements IProvisionService { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final Vertx vertx; + private final TemplateEngine engine; + + ProvisionServiceImpl(@Nonnull Vertx vertx, @Nonnull TemplateEngine engine) { + this.vertx = vertx; + this.engine = engine; + } + + @Nonnull + private static String generateBGPPath(int id) { + return String.format("/etc/bird/peers/dn42_%d.conf", id); + } + + @Nonnull + private static String generateWGPath(@Nonnull String dev) { + return String.format("/etc/wireguard/%s.conf", dev); + } + + @Nonnull + private static String generateWireGuardDevName(long id) { + return String.format("wg_%d", id); + } + + @Nonnull + private static String getLockNameForBGP(long id) { + return String.format("BGP:%d", id); + } + + @Nonnull + private static String getLockNameForWG(long id) { + return String.format("WG:%d", id); + } + + @Nonnull + private Future<Void> writeBGPConfig(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + @Nonnull String ipv4, + @Nullable String ipv6, + @Nullable String device, + boolean mpbgp, + @Nonnull String asn, + boolean create) { + final String asnNum = asn.replace("AS", ""); + return vertx.fileSystem().open(generateBGPPath(id), new OpenOptions() + .setCreateNew(create) + .setTruncateExisting(true) + .setWrite(true)) + .compose(asyncFile -> { + if (mpbgp) return Future.succeededFuture(asyncFile); + final Map<String, Object> params = new HashMap<>(3); + params.put("name", id); + params.put("ipv4", ipv4); + params.put("asn", asnNum); + return engine.render(params, "bird2_v4.conf.ftlh") + .compose(buffer -> asyncFile.write(buffer) + .compose(_v1 -> Future.succeededFuture(asyncFile))); + }) + .compose(asyncFile -> { + if (ipv6 == null) return Future.succeededFuture(asyncFile); + final Map<String, Object> params = new HashMap<>(4); + params.put("name", id); + params.put("ipv6", ipv6); + params.put("asn", asnNum); + params.put("dev", device); + return engine.render(params, "bird2_v6.conf.ftlh") + .compose(buffer -> asyncFile.write(buffer) + .compose(_v1 -> Future.succeededFuture(asyncFile))); + }) + .compose(AsyncFile::close); + } + + @Nonnull + private Future<Void> writeWGConfig(boolean create, + @Nonnull String localIP4, + @Nonnull String localIP6, + @Nonnull String dev, + int listenPort, + @Nonnull String endpointWithPort, + @Nonnull String peerPubKey, + @Nonnull String selfPrivKey, + @Nonnull String selfPresharedSecret, + @Nonnull String peerIPv4, + @Nullable String peerIPv6) { + return vertx.fileSystem().open(generateWGPath(dev), new OpenOptions() + .setCreateNew(create) + .setTruncateExisting(true) + .setWrite(true)) + .compose(asyncFile -> { + final Map<String, Object> params = new HashMap<>(9); + params.put("listen_port", listenPort); + params.put("self_priv_key", selfPrivKey); + params.put("dev", dev); + params.put("self_ipv4", localIP4); + params.put("peer_ipv4", peerIPv4); + params.put("peer_ipv6", peerIPv6); + if (peerIPv6 != null) { + try { + params.put("peer_ipv6_ll", Inet6Address.getByName(peerIPv6).isLinkLocalAddress()); + } catch (IOException e) { + return Future.failedFuture(e); + } + } + params.put("self_ipv6", localIP6); + params.put("preshared_key", selfPresharedSecret); + params.put("endpoint", endpointWithPort); + params.put("peer_pub_key", peerPubKey); + + return engine.render(params, "wg.conf.ftlh") + .compose(buffer -> asyncFile.write(buffer) + .compose(_v1 -> Future.succeededFuture(asyncFile))); + }) + .compose(AsyncFile::close); + } + + @Nonnull + private Future<Void> deleteBGPConfig(int id) { + return vertx.fileSystem().delete(generateBGPPath(id)); + } + + @Nonnull + private Future<Void> deleteWGConfig(@Nonnull String dev) { + return vertx.fileSystem().delete(generateWGPath(dev)); + } + + @Nonnull + @Override + public IProvisionService provisionBGP(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + @Nonnull String ipv4, + @Nullable String ipv6, + @Nullable String device, + boolean mpbgp, + @Nonnull String asn, + @Nonnull Handler<AsyncResult<Void>> handler) { + vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000) + .compose(lock -> + writeBGPConfig(localIP4, localIP6, id, ipv4, ipv6, device, mpbgp, asn, true) + .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure")) + .onComplete(ar -> lock.release()) + ) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IProvisionService reloadBGP(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + @Nonnull String ipv4, + @Nullable String ipv6, + @Nullable String device, + boolean mpbgp, + @Nonnull String asn, + @Nonnull Handler<AsyncResult<Void>> handler) { + vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000) + .compose(lock -> + writeBGPConfig(localIP4, localIP6, id, ipv4, ipv6, device, mpbgp, asn, false) + .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure")) + .onComplete(ar -> lock.release()) + ) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IProvisionService unprovisionBGP(int id, @Nonnull Handler<AsyncResult<Void>> handler) { + vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000) + .compose(lock -> + deleteBGPConfig(id) + .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure")) + .onComplete(ar -> lock.release()) + ) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IProvisionService provisionVPNWireGuard(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + int listenPort, + @Nonnull String endpointWithPort, + @Nonnull String peerPubKey, + @Nonnull String selfPrivKey, + @Nonnull String selfPresharedSecret, + @Nonnull String peerIPv4, + @Nullable String peerIPv6, + @Nonnull Handler<AsyncResult<String>> handler) { + vertx.sharedData() + .getLocalLockWithTimeout(getLockNameForWG(id), 1000) + .compose(lock -> writeWGConfig(true, + localIP4, + localIP6, + generateWireGuardDevName(id), + listenPort, + endpointWithPort, + peerPubKey, + selfPrivKey, + selfPresharedSecret, + peerIPv4, + peerIPv6) + .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "enable", "--now", "-q", + String.format("wg-quick@%s", generateWireGuardDevName(id)))) + .onComplete(_v -> lock.release())) + .compose(_v -> Future.succeededFuture(generateWireGuardDevName(id))) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IProvisionService reloadVPNWireGuard(@Nonnull String localIP4, + @Nonnull String localIP6, + int id, + int listenPort, + @Nonnull String endpointWithPort, + @Nonnull String peerPubKey, + @Nonnull String selfPrivKey, + @Nonnull String selfPresharedSecret, + @Nonnull String peerIPv4, + @Nullable String peerIPv6, + @Nonnull Handler<AsyncResult<String>> handler) { + vertx.sharedData() + .getLocalLockWithTimeout(getLockNameForWG(id), 1000) + .compose(lock -> writeWGConfig(false, + localIP4, + localIP6, + generateWireGuardDevName(id), + listenPort, + endpointWithPort, + peerPubKey, + selfPrivKey, + selfPresharedSecret, + peerIPv4, + peerIPv6) + .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "enable", "-q", + String.format("wg-quick@%s", generateWireGuardDevName(id)))) + .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "reload-or-restart", + String.format("wg-quick@%s", generateWireGuardDevName(id)))) + .onComplete(_v -> lock.release())) + .compose(_v -> Future.succeededFuture(generateWireGuardDevName(id))) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IProvisionService unprovisionVPNWireGuard(int id, @Nonnull Handler<AsyncResult<Void>> handler) { + vertx.sharedData() + .getLocalLockWithTimeout(getLockNameForWG(id), 1000) + .compose(lock -> AsyncShell.execSucc(vertx, "systemctl", "disable", "--now", "-q", + String.format("wg-quick@%s", generateWireGuardDevName(id))) + // We need to stop the service first, then delete the configuration. + .compose(_v -> deleteWGConfig(generateWireGuardDevName(id))) + .onComplete(_v -> lock.release())) + .onComplete(handler); + return this; + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java new file mode 100644 index 0000000..eb2c678 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java @@ -0,0 +1,36 @@ +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +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.templ.freemarker.FreeMarkerTemplateEngine; +import io.vertx.serviceproxy.ServiceBinder; + +public class ProvisionVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private MessageConsumer<JsonObject> consumer; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + consumer = new ServiceBinder(vertx) + .setAddress(IProvisionService.ADDRESS) + .register(IProvisionService.class, new ProvisionServiceImpl(vertx, + FreeMarkerTemplateEngine.create(vertx, "ftlh"))); + consumer.completionHandler(ar -> { + if (ar.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(ar.cause()); + } + }); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + consumer.unregister(stopPromise); + } +} diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java new file mode 100644 index 0000000..672bd29 --- /dev/null +++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(groupPackage = "moe.yuuta.dn42peering.agent.provision", name = "provision") +package moe.yuuta.dn42peering.agent.provision; + +import io.vertx.codegen.annotations.ModuleGen;
\ No newline at end of file diff --git a/agent/src/main/resources/bird2_v4.conf.ftlh b/agent/src/main/resources/bird2_v4.conf.ftlh new file mode 100644 index 0000000..e698069 --- /dev/null +++ b/agent/src/main/resources/bird2_v4.conf.ftlh @@ -0,0 +1,5 @@ +protocol bgp ${name} from dnpeers { + neighbor ${ipv4} as ${asn}; + direct; +} + diff --git a/agent/src/main/resources/bird2_v6.conf.ftlh b/agent/src/main/resources/bird2_v6.conf.ftlh new file mode 100644 index 0000000..d8f4306 --- /dev/null +++ b/agent/src/main/resources/bird2_v6.conf.ftlh @@ -0,0 +1,4 @@ +protocol bgp ${name}_v6 from dnpeers { + neighbor ${ipv6}%${dev} as ${asn}; + direct; +}
\ No newline at end of file diff --git a/agent/src/main/resources/wg.conf.ftlh b/agent/src/main/resources/wg.conf.ftlh new file mode 100644 index 0000000..57ff790 --- /dev/null +++ b/agent/src/main/resources/wg.conf.ftlh @@ -0,0 +1,20 @@ +# Automatically Generated by dn42peering Agent. + +[Interface] +ListenPort = ${listen_port?long?c} +PrivateKey = ${self_priv_key} +PostUp = ip addr add dev ${dev} ${self_ipv4}/32 peer ${peer_ipv4}/32 +<#if peer_ipv6??> +<#if peer_ipv6_ll> +PostUp = ip addr add dev ${dev} ${self_ipv6}/64 +<#else> +PostUp = ip addr add dev ${dev} ${self_ipv6}/64 peer ${peer_ipv6}/64 +</#if> +</#if> +Table = off + +[Peer] +PublicKey = ${peer_pub_key} +PresharedKey = ${preshared_key} +Endpoint = ${endpoint} +AllowedIPs = 0.0.0.0/0, ::/0 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cc98403 --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +allprojects { + repositories { + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + mavenContent { + snapshotsOnly() + } + } + + mavenCentral() + jcenter() + } +} + +project.ext { + vertxVersion = "4.0.1-SNAPSHOT" + grpcVersion = '1.34.1' + protobufVersion = '3.14.0' +} + diff --git a/central/build.gradle b/central/build.gradle new file mode 100644 index 0000000..c4eb3b0 --- /dev/null +++ b/central/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java' + id 'application' +} + +group 'moe.yuuta' +version '1.0' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +application { + getMainClass().set('moe.yuuta.dn42peering.Main') +} + +dependencies { + compileOnly 'com.google.code.findbugs:jsr305:3.0.2' + implementation 'org.apache.commons:commons-text:1.9' + implementation 'commons-net:commons-net:3.7.2' + + implementation 'commons-validator:commons-validator:1.7' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + + implementation "io.vertx:vertx-core:${project.vertxVersion}" + implementation "io.vertx:vertx-web:${project.vertxVersion}" + implementation "io.vertx:vertx-web-templ-freemarker:${project.vertxVersion}" + implementation "io.vertx:vertx-web-validation:${project.vertxVersion}" + implementation "io.vertx:vertx-auth-common:${project.vertxVersion}" + implementation "io.vertx:vertx-mysql-client:${project.vertxVersion}" + implementation "io.vertx:vertx-mail-client:${project.vertxVersion}" + implementation "io.vertx:vertx-service-proxy:${project.vertxVersion}" + implementation "io.vertx:vertx-sql-client-templates:${project.vertxVersion}" + implementation "io.vertx:vertx-config:${project.vertxVersion}" + annotationProcessor "io.vertx:vertx-sql-client-templates:${project.vertxVersion}" + annotationProcessor "io.vertx:vertx-codegen:${project.vertxVersion}:processor" + annotationProcessor "io.vertx:vertx-service-proxy:${project.vertxVersion}" + compileOnly "io.vertx:vertx-codegen:${project.vertxVersion}" + implementation "io.vertx:vertx-grpc:${project.vertxVersion}" + + implementation project(':rpc-common') +} + +test { + useJUnitPlatform() +}
\ No newline at end of file diff --git a/central/src/main/java/com/wireguard/crypto/Curve25519.java b/central/src/main/java/com/wireguard/crypto/Curve25519.java new file mode 100644 index 0000000..ad392f1 --- /dev/null +++ b/central/src/main/java/com/wireguard/crypto/Curve25519.java @@ -0,0 +1,501 @@ +/* + * Copyright © 2016 Southern Storm Software, Pty Ltd. + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java + */ + +package com.wireguard.crypto; + +import com.wireguard.util.NonNullForAll; + +import javax.annotation.Nullable; +import java.util.Arrays; + +/** + * Implementation of the Curve25519 elliptic curve algorithm. + * <p> + * This implementation was imported to WireGuard from noise-java: + * https://github.com/rweather/noise-java + * <p> + * This implementation is based on that from arduinolibs: + * https://github.com/rweather/arduinolibs + * <p> + * Differences in this version are due to using 26-bit limbs for the + * representation instead of the 8/16/32-bit limbs in the original. + * <p> + * References: http://cr.yp.to/ecdh.html, RFC 7748 + */ +@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"}) +@NonNullForAll +public final class Curve25519 { + // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words. + private static final int NUM_LIMBS_255BIT = 10; + private static final int NUM_LIMBS_510BIT = 20; + + private final int[] A; + private final int[] AA; + private final int[] B; + private final int[] BB; + private final int[] C; + private final int[] CB; + private final int[] D; + private final int[] DA; + private final int[] E; + private final long[] t1; + private final int[] t2; + private final int[] x_1; + private final int[] x_2; + private final int[] x_3; + private final int[] z_2; + private final int[] z_3; + + /** + * Constructs the temporary state holder for Curve25519 evaluation. + */ + private Curve25519() { + // Allocate memory for all of the temporary variables we will need. + x_1 = new int[NUM_LIMBS_255BIT]; + x_2 = new int[NUM_LIMBS_255BIT]; + x_3 = new int[NUM_LIMBS_255BIT]; + z_2 = new int[NUM_LIMBS_255BIT]; + z_3 = new int[NUM_LIMBS_255BIT]; + A = new int[NUM_LIMBS_255BIT]; + B = new int[NUM_LIMBS_255BIT]; + C = new int[NUM_LIMBS_255BIT]; + D = new int[NUM_LIMBS_255BIT]; + E = new int[NUM_LIMBS_255BIT]; + AA = new int[NUM_LIMBS_255BIT]; + BB = new int[NUM_LIMBS_255BIT]; + DA = new int[NUM_LIMBS_255BIT]; + CB = new int[NUM_LIMBS_255BIT]; + t1 = new long[NUM_LIMBS_510BIT]; + t2 = new int[NUM_LIMBS_510BIT]; + } + + /** + * Conditional swap of two values. + * + * @param select Set to 1 to swap, 0 to leave as-is. + * @param x The first value. + * @param y The second value. + */ + private static void cswap(int select, final int[] x, final int[] y) { + select = -select; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { + final int dummy = select & (x[index] ^ y[index]); + x[index] ^= dummy; + y[index] ^= dummy; + } + } + + /** + * Evaluates the Curve25519 curve. + * + * @param result Buffer to place the result of the evaluation into. + * @param offset Offset into the result buffer. + * @param privateKey The private key to use in the evaluation. + * @param publicKey The public key to use in the evaluation, or null + * if the base point of the curve should be used. + */ + public static void eval(final byte[] result, final int offset, + final byte[] privateKey, @Nullable final byte[] publicKey) { + final Curve25519 state = new Curve25519(); + try { + // Unpack the public key value. If null, use 9 as the base point. + Arrays.fill(state.x_1, 0); + if (publicKey != null) { + // Convert the input value from little-endian into 26-bit limbs. + for (int index = 0; index < 32; ++index) { + final int bit = (index * 8) % 26; + final int word = (index * 8) / 26; + final int value = publicKey[index] & 0xFF; + if (bit <= (26 - 8)) { + state.x_1[word] |= value << bit; + } else { + state.x_1[word] |= value << bit; + state.x_1[word] &= 0x03FFFFFF; + state.x_1[word + 1] |= value >> (26 - bit); + } + } + + // Just in case, we reduce the number modulo 2^255 - 19 to + // make sure that it is in range of the field before we start. + // This eliminates values between 2^255 - 19 and 2^256 - 1. + state.reduceQuick(state.x_1); + state.reduceQuick(state.x_1); + } else { + state.x_1[0] = 9; + } + + // Initialize the other temporary variables. + Arrays.fill(state.x_2, 0); // x_2 = 1 + state.x_2[0] = 1; + Arrays.fill(state.z_2, 0); // z_2 = 0 + System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1 + Arrays.fill(state.z_3, 0); // z_3 = 1 + state.z_3[0] = 1; + + // Evaluate the curve for every bit of the private key. + state.evalCurve(privateKey); + + // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19. + state.recip(state.z_3, state.z_2); + state.mul(state.x_2, state.x_2, state.z_3); + + // Convert x_2 into little-endian in the result buffer. + for (int index = 0; index < 32; ++index) { + final int bit = (index * 8) % 26; + final int word = (index * 8) / 26; + if (bit <= (26 - 8)) + result[offset + index] = (byte) (state.x_2[word] >> bit); + else + result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit))); + } + } finally { + // Clean up all temporary state before we exit. + state.destroy(); + } + } + + /** + * Subtracts two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to subtract. + * @param y The second number to subtract. + */ + private static void sub(final int[] result, final int[] x, final int[] y) { + int index; + int borrow; + + // Subtract y from x to generate the intermediate result. + borrow = 0; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) { + borrow = x[index] - y[index] - ((borrow >> 26) & 0x01); + result[index] = borrow & 0x03FFFFFF; + } + + // If we had a borrow, then the result has gone negative and we + // have to add 2^255 - 19 to the result to make it positive again. + // The top bits of "borrow" will be all 1's if there is a borrow + // or it will be all 0's if there was no borrow. Easiest is to + // conditionally subtract 19 and then mask off the high bits. + borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19); + result[0] = borrow & 0x03FFFFFF; + for (index = 1; index < NUM_LIMBS_255BIT; ++index) { + borrow = result[index] - ((borrow >> 26) & 0x01); + result[index] = borrow & 0x03FFFFFF; + } + result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + } + + /** + * Adds two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to add. + * @param y The second number to add. + */ + private void add(final int[] result, final int[] x, final int[] y) { + int carry = x[0] + y[0]; + result[0] = carry & 0x03FFFFFF; + for (int index = 1; index < NUM_LIMBS_255BIT; ++index) { + carry = (carry >> 26) + x[index] + y[index]; + result[index] = carry & 0x03FFFFFF; + } + reduceQuick(result); + } + + /** + * Destroy all sensitive data in this object. + */ + private void destroy() { + // Destroy all temporary variables. + Arrays.fill(x_1, 0); + Arrays.fill(x_2, 0); + Arrays.fill(x_3, 0); + Arrays.fill(z_2, 0); + Arrays.fill(z_3, 0); + Arrays.fill(A, 0); + Arrays.fill(B, 0); + Arrays.fill(C, 0); + Arrays.fill(D, 0); + Arrays.fill(E, 0); + Arrays.fill(AA, 0); + Arrays.fill(BB, 0); + Arrays.fill(DA, 0); + Arrays.fill(CB, 0); + Arrays.fill(t1, 0L); + Arrays.fill(t2, 0); + } + + /** + * Evaluates the curve for every bit in a secret key. + * + * @param s The 32-byte secret key. + */ + private void evalCurve(final byte[] s) { + int sposn = 31; + int sbit = 6; + int svalue = s[sposn] | 0x40; + int swap = 0; + + // Iterate over all 255 bits of "s" from the highest to the lowest. + // We ignore the high bit of the 256-bit representation of "s". + while (true) { + // Conditional swaps on entry to this bit but only if we + // didn't swap on the previous bit. + final int select = (svalue >> sbit) & 0x01; + swap ^= select; + cswap(swap, x_2, x_3); + cswap(swap, z_2, z_3); + swap = select; + + // Evaluate the curve. + add(A, x_2, z_2); // A = x_2 + z_2 + square(AA, A); // AA = A^2 + sub(B, x_2, z_2); // B = x_2 - z_2 + square(BB, B); // BB = B^2 + sub(E, AA, BB); // E = AA - BB + add(C, x_3, z_3); // C = x_3 + z_3 + sub(D, x_3, z_3); // D = x_3 - z_3 + mul(DA, D, A); // DA = D * A + mul(CB, C, B); // CB = C * B + add(x_3, DA, CB); // x_3 = (DA + CB)^2 + square(x_3, x_3); + sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2 + square(z_3, z_3); + mul(z_3, z_3, x_1); + mul(x_2, AA, BB); // x_2 = AA * BB + mulA24(z_2, E); // z_2 = E * (AA + a24 * E) + add(z_2, z_2, AA); + mul(z_2, z_2, E); + + // Move onto the next lower bit of "s". + if (sbit > 0) { + --sbit; + } else if (sposn == 0) { + break; + } else if (sposn == 1) { + --sposn; + svalue = s[sposn] & 0xF8; + sbit = 7; + } else { + --sposn; + svalue = s[sposn]; + sbit = 7; + } + } + + // Final conditional swaps. + cswap(swap, x_2, x_3); + cswap(swap, z_2, z_3); + } + + /** + * Multiplies two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to multiply. + * @param y The second number to multiply. + */ + private void mul(final int[] result, final int[] x, final int[] y) { + // Multiply the two numbers to create the intermediate result. + long v = x[0]; + for (int i = 0; i < NUM_LIMBS_255BIT; ++i) { + t1[i] = v * y[i]; + } + for (int i = 1; i < NUM_LIMBS_255BIT; ++i) { + v = x[i]; + for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) { + t1[i + j] += v * y[j]; + } + t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1]; + } + + // Propagate carries and convert back into 26-bit words. + v = t1[0]; + t2[0] = ((int) v) & 0x03FFFFFF; + for (int i = 1; i < NUM_LIMBS_510BIT; ++i) { + v = (v >> 26) + t1[i]; + t2[i] = ((int) v) & 0x03FFFFFF; + } + + // Reduce the result modulo 2^255 - 19. + reduce(result, t2, NUM_LIMBS_255BIT); + } + + /** + * Multiplies a number by the a24 constant, modulo 2^255 - 19. + * + * @param result The result. + * @param x The number to multiply by a24. + */ + private void mulA24(final int[] result, final int[] x) { + final long a24 = 121665; + long carry = 0; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += a24 * x[index]; + t2[index] = ((int) carry) & 0x03FFFFFF; + carry >>= 26; + } + t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF; + reduce(result, t2, 1); + } + + /** + * Raise x to the power of (2^250 - 1). + * + * @param result The result. Must not overlap with x. + * @param x The argument. + */ + private void pow250(final int[] result, final int[] x) { + // The big-endian hexadecimal expansion of (2^250 - 1) is: + // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF + // + // The naive implementation needs to do 2 multiplications per 1 bit and + // 1 multiplication per 0 bit. We can improve upon this by creating a + // pattern 0000000001 ... 0000000001. If we square and multiply the + // pattern by itself we can turn the pattern into the partial results + // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc. + // This averages out to about 1.1 multiplications per 1 bit instead of 2. + + // Build a pattern of 250 bits in length of repeated copies of 0000000001. + square(A, x); + for (int j = 0; j < 9; ++j) + square(A, A); + mul(result, A, x); + for (int i = 0; i < 23; ++i) { + for (int j = 0; j < 10; ++j) + square(A, A); + mul(result, result, A); + } + + // Multiply bit-shifted versions of the 0000000001 pattern into + // the result to "fill in" the gaps in the pattern. + square(A, result); + mul(result, result, A); + for (int j = 0; j < 8; ++j) { + square(A, A); + mul(result, result, A); + } + } + + /** + * Computes the reciprocal of a number modulo 2^255 - 19. + * + * @param result The result. Must not overlap with x. + * @param x The argument. + */ + private void recip(final int[] result, final int[] x) { + // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19. + // The big-endian hexadecimal expansion of (p - 2) is: + // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB + // Start with the 250 upper bits of the expansion of (p - 2). + pow250(result, x); + + // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest. + square(result, result); + square(result, result); + mul(result, result, x); + square(result, result); + square(result, result); + mul(result, result, x); + square(result, result); + mul(result, result, x); + } + + /** + * Reduce a number modulo 2^255 - 19. + * + * @param result The result. + * @param x The value to be reduced. This array will be + * modified during the reduction. + * @param size The number of limbs in the high order half of x. + */ + private void reduce(final int[] result, final int[] x, final int size) { + // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will + // either produce the answer we want or it will produce a + // value of the form "answer + j * (2^255 - 19)". There are + // 5 left-over bits in the top-most limb of the bottom half. + int carry = 0; + int limb = x[NUM_LIMBS_255BIT - 1] >> 21; + x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (int index = 0; index < size; ++index) { + limb += x[NUM_LIMBS_255BIT + index] << 5; + carry += (limb & 0x03FFFFFF) * 19 + x[index]; + x[index] = carry & 0x03FFFFFF; + limb >>= 26; + carry >>= 26; + } + if (size < NUM_LIMBS_255BIT) { + // The high order half of the number is short; e.g. for mulA24(). + // Propagate the carry through the rest of the low order part. + for (int index = size; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + x[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + } + + // The "j" value may still be too large due to the final carry-out. + // We must repeat the reduction. If we already have the answer, + // then this won't do any harm but we must still do the calculation + // to preserve the overall timing. The "j" value will be between + // 0 and 19, which means that the carry we care about is in the + // top 5 bits of the highest limb of the bottom half. + carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19; + x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + result[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + + // At this point "x" will either be the answer or it will be the + // answer plus (2^255 - 19). Perform a trial subtraction to + // complete the reduction process. + reduceQuick(result); + } + + /** + * Reduces a number modulo 2^255 - 19 where it is known that the + * number can be reduced with only 1 trial subtraction. + * + * @param x The number to reduce, and the result. + */ + private void reduceQuick(final int[] x) { + // Perform a trial subtraction of (2^255 - 19) from "x" which is + // equivalent to adding 19 and subtracting 2^255. We add 19 here; + // the subtraction of 2^255 occurs in the next step. + int carry = 19; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + t2[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + + // If there was a borrow, then the original "x" is the correct answer. + // If there was no borrow, then "t2" is the correct answer. Select the + // correct answer but do it in a way that instruction timing will not + // reveal which value was selected. Borrow will occur if bit 21 of + // "t2" is zero. Turn the bit into a selection mask. + final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01); + final int nmask = ~mask; + t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) + x[index] = (x[index] & nmask) | (t2[index] & mask); + } + + /** + * Squares a number modulo 2^255 - 19. + * + * @param result The result. + * @param x The number to square. + */ + private void square(final int[] result, final int[] x) { + mul(result, x, x); + } +}
\ No newline at end of file diff --git a/central/src/main/java/com/wireguard/crypto/Key.java b/central/src/main/java/com/wireguard/crypto/Key.java new file mode 100644 index 0000000..27edcd9 --- /dev/null +++ b/central/src/main/java/com/wireguard/crypto/Key.java @@ -0,0 +1,292 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/Key.java + */ + +package com.wireguard.crypto; + +import com.wireguard.crypto.KeyFormatException.Type; +import com.wireguard.util.NonNullForAll; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * Represents a WireGuard public or private key. This class uses specialized constant-time base64 + * and hexadecimal codec implementations that resist side-channel attacks. + * <p> + * Instances of this class are immutable. + */ +@SuppressWarnings("MagicNumber") +@NonNullForAll +public final class Key { + private final byte[] key; + + /** + * Constructs an object encapsulating the supplied key. + * + * @param key an array of bytes containing a binary key. Callers of this constructor are + * responsible for ensuring that the array is of the correct length. + */ + private Key(final byte[] key) { + // Defensively copy to ensure immutability. + this.key = Arrays.copyOf(key, key.length); + } + + /** + * Decodes a single 4-character base64 chunk to an integer in constant time. + * + * @param src an array of at least 4 characters in base64 format + * @param srcOffset the offset of the beginning of the chunk in {@code src} + * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not + * valid base64 + */ + private static int decodeBase64(final char[] src, final int srcOffset) { + int val = 0; + for (int i = 0; i < 4; ++i) { + final char c = src[i + srcOffset]; + val |= (-1 + + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64)) + + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70)) + + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5)) + + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63) + + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64) + ) << (18 - 6 * i); + } + return val; + } + + /** + * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time. + * + * @param src an array of at least 3 bytes + * @param srcOffset the offset of the beginning of the chunk in {@code src} + * @param dest an array of at least 4 characters + * @param destOffset the offset of the beginning of the chunk in {@code dest} + */ + private static void encodeBase64(final byte[] src, final int srcOffset, + final char[] dest, final int destOffset) { + final byte[] input = { + (byte) ((src[srcOffset] >>> 2) & 63), + (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63), + (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63), + (byte) ((src[2 + srcOffset]) & 63), + }; + for (int i = 0; i < 4; ++i) { + dest[i + destOffset] = (char) (input[i] + 'A' + + (((25 - input[i]) >>> 8) & 6) + - (((51 - input[i]) >>> 8) & 75) + - (((61 - input[i]) >>> 8) & 15) + + (((62 - input[i]) >>> 8) & 3)); + } + } + + /** + * Decodes a WireGuard public or private key from its base64 string representation. This + * function throws a {@link KeyFormatException} if the source string is not well-formed. + * + * @param str the base64 string representation of a WireGuard key + * @return the decoded key encapsulated in an immutable container + */ + public static Key fromBase64(final String str) throws KeyFormatException { + final char[] input = str.toCharArray(); + if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') + throw new KeyFormatException(Format.BASE64, Type.LENGTH); + final byte[] key = new byte[Format.BINARY.length]; + int i; + int ret = 0; + for (i = 0; i < key.length / 3; ++i) { + final int val = decodeBase64(input, i * 4); + ret |= val >>> 31; + key[i * 3] = (byte) ((val >>> 16) & 0xff); + key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); + key[i * 3 + 2] = (byte) (val & 0xff); + } + final char[] endSegment = { + input[i * 4], + input[i * 4 + 1], + input[i * 4 + 2], + 'A', + }; + final int val = decodeBase64(endSegment, 0); + ret |= (val >>> 31) | (val & 0xff); + key[i * 3] = (byte) ((val >>> 16) & 0xff); + key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); + + if (ret != 0) + throw new KeyFormatException(Format.BASE64, Type.CONTENTS); + return new Key(key); + } + + /** + * Wraps a WireGuard public or private key in an immutable container. This function throws a + * {@link KeyFormatException} if the source data is not the correct length. + * + * @param bytes an array of bytes containing a WireGuard key in binary format + * @return the key encapsulated in an immutable container + */ + public static Key fromBytes(final byte[] bytes) throws KeyFormatException { + if (bytes.length != Format.BINARY.length) + throw new KeyFormatException(Format.BINARY, Type.LENGTH); + return new Key(bytes); + } + + /** + * Decodes a WireGuard public or private key from its hexadecimal string representation. This + * function throws a {@link KeyFormatException} if the source string is not well-formed. + * + * @param str the hexadecimal string representation of a WireGuard key + * @return the decoded key encapsulated in an immutable container + */ + public static Key fromHex(final String str) throws KeyFormatException { + final char[] input = str.toCharArray(); + if (input.length != Format.HEX.length) + throw new KeyFormatException(Format.HEX, Type.LENGTH); + final byte[] key = new byte[Format.BINARY.length]; + int ret = 0; + for (int i = 0; i < key.length; ++i) { + int c; + int cNum; + int cNum0; + int cAlpha; + int cAlpha0; + int cVal; + final int cAcc; + + c = input[i * 2]; + cNum = c ^ 48; + cNum0 = ((cNum - 10) >>> 8) & 0xff; + cAlpha = (c & ~32) - 55; + cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; + ret |= ((cNum0 | cAlpha0) - 1) >>> 8; + cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); + cAcc = cVal * 16; + + c = input[i * 2 + 1]; + cNum = c ^ 48; + cNum0 = ((cNum - 10) >>> 8) & 0xff; + cAlpha = (c & ~32) - 55; + cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; + ret |= ((cNum0 | cAlpha0) - 1) >>> 8; + cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); + key[i] = (byte) (cAcc | cVal); + } + if (ret != 0) + throw new KeyFormatException(Format.HEX, Type.CONTENTS); + return new Key(key); + } + + /** + * Generates a private key using the system's {@link SecureRandom} number generator. + * + * @return a well-formed random private key + */ + public static Key generatePrivateKey() { + final SecureRandom secureRandom = new SecureRandom(); + final byte[] privateKey = new byte[Format.BINARY.getLength()]; + secureRandom.nextBytes(privateKey); + privateKey[0] &= 248; + privateKey[31] &= 127; + privateKey[31] |= 64; + return new Key(privateKey); + } + + /** + * Generates a public key from an existing private key. + * + * @param privateKey a private key + * @return a well-formed public key that corresponds to the supplied private key + */ + public static Key generatePublicKey(final Key privateKey) { + final byte[] publicKey = new byte[Format.BINARY.getLength()]; + Curve25519.eval(publicKey, 0, privateKey.getBytes(), null); + return new Key(publicKey); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != getClass()) + return false; + final Key other = (Key) obj; + return MessageDigest.isEqual(key, other.key); + } + + /** + * Returns the key as an array of bytes. + * + * @return an array of bytes containing the raw binary key + */ + public byte[] getBytes() { + // Defensively copy to ensure immutability. + return Arrays.copyOf(key, key.length); + } + + @Override + public int hashCode() { + int ret = 0; + for (int i = 0; i < key.length / 4; ++i) + ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24); + return ret; + } + + /** + * Encodes the key to base64. + * + * @return a string containing the encoded key + */ + public String toBase64() { + final char[] output = new char[Format.BASE64.length]; + int i; + for (i = 0; i < key.length / 3; ++i) + encodeBase64(key, i * 3, output, i * 4); + final byte[] endSegment = { + key[i * 3], + key[i * 3 + 1], + 0, + }; + encodeBase64(endSegment, 0, output, i * 4); + output[Format.BASE64.length - 1] = '='; + return new String(output); + } + + /** + * Encodes the key to hexadecimal ASCII characters. + * + * @return a string containing the encoded key + */ + public String toHex() { + final char[] output = new char[Format.HEX.length]; + for (int i = 0; i < key.length; ++i) { + output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) + + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38)); + output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) + + ((((key[i] & 0xf) - 10) >> 8) & ~38)); + } + return new String(output); + } + + /** + * The supported formats for encoding a WireGuard key. + */ + public enum Format { + BASE64(44), + BINARY(32), + HEX(64); + + private final int length; + + Format(final int length) { + this.length = length; + } + + public int getLength() { + return length; + } + } + +}
\ No newline at end of file diff --git a/central/src/main/java/com/wireguard/crypto/KeyFormatException.java b/central/src/main/java/com/wireguard/crypto/KeyFormatException.java new file mode 100644 index 0000000..21eb592 --- /dev/null +++ b/central/src/main/java/com/wireguard/crypto/KeyFormatException.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java + */ + +package com.wireguard.crypto; + +import com.wireguard.util.NonNullForAll; + +/** + * An exception thrown when attempting to parse an invalid key (too short, too long, or byte + * data inappropriate for the format). The format being parsed can be accessed with the + * {@link #getFormat} method. + */ +@NonNullForAll +public final class KeyFormatException extends Exception { + private final Key.Format format; + private final Type type; + + KeyFormatException(final Key.Format format, final Type type) { + this.format = format; + this.type = type; + } + + public Key.Format getFormat() { + return format; + } + + public Type getType() { + return type; + } + + public enum Type { + CONTENTS, + LENGTH + } +}
\ No newline at end of file diff --git a/central/src/main/java/com/wireguard/util/NonNullForAll.java b/central/src/main/java/com/wireguard/util/NonNullForAll.java new file mode 100644 index 0000000..737fe39 --- /dev/null +++ b/central/src/main/java/com/wireguard/util/NonNullForAll.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java + */ + +package com.wireguard.util; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation can be applied to a package, class or method to indicate that all + * class fields and method parameters and return values in that element are nonnull + * by default unless overridden. + */ +@Nonnull +@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) + +public @interface NonNullForAll { +}
\ No newline at end of file diff --git a/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java b/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java new file mode 100644 index 0000000..159f99d --- /dev/null +++ b/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java @@ -0,0 +1,142 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com) + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * */ + +package edazdarevic.commons.net; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A class that enables to get an IP range from CIDR specification. It supports + * both IPv4 and IPv6. + */ +public class CIDRUtils { + private final String cidr; + + private InetAddress inetAddress; + private InetAddress startAddress; + private InetAddress endAddress; + private final int prefixLength; + + + public CIDRUtils(String cidr) throws UnknownHostException { + + this.cidr = cidr; + + /* split CIDR to address and prefix part */ + if (this.cidr.contains("/")) { + int index = this.cidr.indexOf("/"); + String addressPart = this.cidr.substring(0, index); + String networkPart = this.cidr.substring(index + 1); + + inetAddress = InetAddress.getByName(addressPart); + prefixLength = Integer.parseInt(networkPart); + + calculate(); + } else { + throw new IllegalArgumentException("not an valid CIDR format!"); + } + } + + + private void calculate() throws UnknownHostException { + + ByteBuffer maskBuffer; + int targetSize; + if (inetAddress.getAddress().length == 4) { + maskBuffer = + ByteBuffer + .allocate(4) + .putInt(-1); + targetSize = 4; + } else { + maskBuffer = ByteBuffer.allocate(16) + .putLong(-1L) + .putLong(-1L); + targetSize = 16; + } + + BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength); + + ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress()); + BigInteger ipVal = new BigInteger(1, buffer.array()); + + BigInteger startIp = ipVal.and(mask); + BigInteger endIp = startIp.add(mask.not()); + + byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize); + byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize); + + this.startAddress = InetAddress.getByAddress(startIpArr); + this.endAddress = InetAddress.getByAddress(endIpArr); + + } + + private byte[] toBytes(byte[] array, int targetSize) { + int counter = 0; + List<Byte> newArr = new ArrayList<Byte>(); + while (counter < targetSize && (array.length - 1 - counter >= 0)) { + newArr.add(0, array[array.length - 1 - counter]); + counter++; + } + + int size = newArr.size(); + for (int i = 0; i < (targetSize - size); i++) { + + newArr.add(0, (byte) 0); + } + + byte[] ret = new byte[newArr.size()]; + for (int i = 0; i < newArr.size(); i++) { + ret[i] = newArr.get(i); + } + return ret; + } + + public String getNetworkAddress() { + + return this.startAddress.getHostAddress(); + } + + public String getBroadcastAddress() { + return this.endAddress.getHostAddress(); + } + + public boolean isInRange(String ipAddress) throws UnknownHostException { + InetAddress address = InetAddress.getByName(ipAddress); + BigInteger start = new BigInteger(1, this.startAddress.getAddress()); + BigInteger end = new BigInteger(1, this.endAddress.getAddress()); + BigInteger target = new BigInteger(1, address.getAddress()); + + int st = start.compareTo(target); + int te = target.compareTo(end); + + return (st == -1 || st == 0) && (te == -1 || te == 0); + } +}
\ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java b/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java new file mode 100644 index 0000000..711ab89 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java @@ -0,0 +1,27 @@ +package moe.yuuta.dn42peering; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; + +public class IOUtils { + @Nonnull + public static String read(@Nonnull InputStream in) throws IOException { + StringBuilder data = new StringBuilder(); + while(true) { + int i = in.read(); + if(i == -1) break; + data.append((char)i); + } + in.close(); + return data.toString(); + } + + @Nonnull + public static String readFromResource(@Nonnull ClassLoader cl, @Nonnull String name) throws IOException { + final InputStream in = cl.getResourceAsStream(name); + final String data = read(in); + in.close(); + return data; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/Main.java b/central/src/main/java/moe/yuuta/dn42peering/Main.java new file mode 100644 index 0000000..595cb30 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/Main.java @@ -0,0 +1,52 @@ +package moe.yuuta.dn42peering; + +import io.vertx.core.*; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import moe.yuuta.dn42peering.asn.ASNVerticle; +import moe.yuuta.dn42peering.node.NodeVerticle; +import moe.yuuta.dn42peering.peer.PeerVerticle; +import moe.yuuta.dn42peering.portal.HTTPPortalVerticle; +import moe.yuuta.dn42peering.whois.WhoisVerticle; + +import javax.annotation.Nonnull; +import java.io.FileInputStream; +import java.io.InputStream; + +public class Main { + public static void main(@Nonnull String... args) throws Throwable { + if(args.length != 1) { + System.err.println("Usage: central <path/to/config.json>"); + System.exit(64); + return; + } + + System.setProperty("vertx.logger-delegate-factory-class-name", + "io.vertx.core.logging.JULLogDelegateFactory"); + + final InputStream in = new FileInputStream(args[0]); + final JsonObject config = new JsonObject(IOUtils.read(in)); + in.close(); + + final Vertx vertx = Vertx.vertx(new VertxOptions()); + + final DeploymentOptions options = new DeploymentOptions() + .setConfig(config) + .setInstances(Runtime.getRuntime().availableProcessors() * 2); + Logger logger = LoggerFactory.getLogger("Main"); + CompositeFuture.all( + Future.<String>future(f -> vertx.deployVerticle(PeerVerticle.class.getName(), options, f)), + Future.<String>future(f -> vertx.deployVerticle(WhoisVerticle.class.getName(), options, f)), + Future.<String>future(f -> vertx.deployVerticle(ASNVerticle.class.getName(), options, f)), + Future.<String>future(f -> vertx.deployVerticle(NodeVerticle.class.getName(), options, f)), + Future.<String>future(f -> vertx.deployVerticle(HTTPPortalVerticle.class.getName(), options, f)) + ).onComplete(res -> { + if (res.succeeded()) { + logger.info("The server started."); + } else { + logger.error("Cannot deploy the server.", res.cause()); + } + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java new file mode 100644 index 0000000..2ea2f68 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java @@ -0,0 +1,119 @@ +package moe.yuuta.dn42peering.asn; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.format.SnakeCase; +import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.templates.annotations.Column; +import io.vertx.sqlclient.templates.annotations.ParametersMapped; +import io.vertx.sqlclient.templates.annotations.RowMapped; +import io.vertx.sqlclient.templates.annotations.TemplateParameter; +import moe.yuuta.dn42peering.utils.PasswordAuthentication; + +import javax.annotation.Nonnull; +import java.util.Date; + +// DB Table: asn +@DataObject +@RowMapped(formatter = SnakeCase.class) +@ParametersMapped(formatter = SnakeCase.class) +public class ASN { + @Column(name = "asn") + @TemplateParameter(name = "asn") + @Nonnull + private String asn; + + @Column(name = "password_hash") + @TemplateParameter(name = "password_hash") + @Nonnull + private String passwordHash; + + @Column(name = "activated") + @TemplateParameter(name = "activated") + private boolean activated; + + @Column(name = "register_date") + @TemplateParameter(name = "register_date") + private Date registerDate; + + public ASN() { + this("", "", false, new Date()); + } + + public ASN(@Nonnull String asn, @Nonnull String passwordHash, + boolean activated, + @Nonnull Date registerDate) { + this.asn = asn; + this.passwordHash = passwordHash; + this.activated = activated; + this.registerDate = registerDate; + } + + public ASN(@Nonnull JsonObject jsonObject) { + this(jsonObject.getString("asn"), + jsonObject.getString("password_hash"), + jsonObject.getBoolean("activated"), + new Date(jsonObject.getLong("register_date"))); + } + + /** + * Create an ASN object when registering using random password. + */ + public ASN(@Nonnull String asn, @Nonnull String randomPassword) { + this(asn, + new PasswordAuthentication() + .hash(randomPassword.toCharArray()), + false, + new Date()); + } + + @Nonnull + public JsonObject toJson() { + return new JsonObject() + .put("asn", asn) + .put("password_hash", passwordHash) + .put("activated", activated) + .put("register_date", registerDate.getTime()); + } + + public final boolean auth(@Nonnull String password) { + return new PasswordAuthentication().authenticate(password.toCharArray(), passwordHash); + } + + public String getAsn() { + return asn; + } + + public boolean isActivated() { + return activated; + } + + public Date getRegisterDate() { + return registerDate; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + // BEGIN GETTERS / SETTERS + + public void setAsn(@Nonnull String asn) { + this.asn = asn; + } + + @Nonnull + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(@Nonnull String passwordHash) { + this.passwordHash = passwordHash; + } + + public void setRegisterDate(Date registerDate) { + this.registerDate = registerDate; + } + + + // END GETTERS / SETTERS +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java new file mode 100644 index 0000000..b498fe1 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java @@ -0,0 +1,202 @@ +package moe.yuuta.dn42peering.asn; + +import io.vertx.core.*; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.mail.MailClient; +import io.vertx.ext.mail.MailConfig; +import io.vertx.ext.mail.MailMessage; +import io.vertx.ext.mail.MailResult; +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 moe.yuuta.dn42peering.jaba.Pair; +import moe.yuuta.dn42peering.portal.FormException; +import moe.yuuta.dn42peering.portal.HTTPException; +import moe.yuuta.dn42peering.portal.ISubRouter; +import moe.yuuta.dn42peering.whois.IWhoisService; +import moe.yuuta.dn42peering.whois.WhoisObject; +import org.apache.commons.text.RandomStringGenerator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.vertx.json.schema.common.dsl.Schemas.objectSchema; +import static io.vertx.json.schema.common.dsl.Schemas.stringSchema; + +public class ASNHandler 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 IWhoisService whoisService = IWhoisService.createProxy(vertx, IWhoisService.ADDRESS); + // Preserve. We need to get email address out of it later. + final JsonObject mailConfig = vertx.getOrCreateContext().config().getJsonObject("mail"); + final MailClient mailClient = MailClient.create(vertx, new MailConfig(mailConfig)); + + final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh"); + final Router router = Router.router(vertx); + router.get("/") + .produces("text/html") + .handler(ctx -> { + renderIndex(engine, null, null, res -> { + if(res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + logger.error("Cannot render /asn.", res.cause()); + } + }); + }); + + final ObjectSchemaBuilder registerSchema = objectSchema() + .allowAdditionalProperties(false) + .requiredProperty("asn", stringSchema()); + final SchemaParser parser = SchemaParser.createDraft7SchemaParser( + SchemaRouter.create(vertx, new SchemaRouterOptions())); + router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024)); + router.post("/") + .produces("text/html") + .handler(ValidationHandler + .builder(parser) + .body(Bodies.formUrlEncoded(registerSchema)) + .predicate(RequestPredicate.BODY_REQUIRED) + .build()) + .handler(ctx -> { + final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) + .body().getJsonObject(); + final String upperASN = parameters.getString("asn").toUpperCase(); + // Start: Check if the ASN exists. + Future.<Void>future(f -> asnService.exists(upperASN, true, true, ar -> { + if(ar.succeeded()) { + if(ar.result()) { + f.fail(new FormException("This ASN exists in our records. Please login instead of registering.")); + } else { + f.complete(); + } + } else { + f.fail(ar.cause()); + } + })) + // Lookup ASN + .<WhoisObject>compose(exists -> + Future.future(f -> whoisService.query(upperASN, f))) + // Lookup emails + .<List<String>>compose(asnLookup -> { + if(asnLookup == null) { + return Future.failedFuture(new FormException("The ASN is not found in the DN42 registry.")); + } else { + return Future.future(f -> asnService.lookupEmails(asnLookup, ar -> { + if(ar.succeeded()) { + if(ar.result().isEmpty()) { + f.fail(new FormException("The tech-c contact for this ASN does not have emails.")); + } else { + f.complete(ar.result()); + } + } else { + f.fail(ar.cause()); + } + })); + } + }) + // Generate random password and register. + .<Pair<String /* Random password */, List<String> /* Emails */>>compose(emails -> Future.future(f -> { + final String randomPassword = new RandomStringGenerator.Builder() + .withinRange('a', 'z') + .build() + .generate(15); + asnService.registerOrChangePassword(upperASN, randomPassword, ar -> { + if(ar.succeeded()) { + f.complete(new Pair<>(randomPassword, emails)); + } else { + f.fail(ar.cause()); + } + }); + })) + // Send mails. + .compose(pair -> CompositeFuture.any(Stream.of(pair.b) + .map(mail -> new MailMessage() + .setFrom(mailConfig.getString("from")) + .setTo(mail) + .setText(String.format("Hi %s! Welcome to dn42 peering! Your peering initial password is %s. Make sure to change it.", + upperASN, + pair.a)) + .setSubject("Peering initial password")) + .map(message -> Future.<MailResult>future(f -> mailClient.sendMail(message, f))) + .collect(Collectors.toList()))) + // Render HTML or report errors + .onComplete(ar -> { + if(ar.succeeded()) { + // Get MailResult's out of the future. + final List<MailResult> sendRes = ar.result().list(); + renderSuccess(engine, sendRes, res -> { + if(res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + logger.error("Cannot render /asn (success).", res.cause()); + } + }); + } else { + if(ar.cause() instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) ar.cause()).code).end(); + } else if(ar.cause() instanceof FormException) { + renderIndex(engine, + Arrays.asList(((FormException) ar.cause()).errors.clone()), + upperASN, + res -> { + if(res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + logger.error("Cannot render /asn (with errors).", res.cause()); + } + }); + } else { + logger.error(String.format("Cannot register ASN %s.", upperASN), ar.cause()); + ctx.fail(ar.cause()); + } + } + }); + }); + + return router; + } + + private void renderIndex(@Nonnull TemplateEngine engine, + @Nullable List<String> errors, + @Nullable String asn, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + final Map<String, Object> root = new HashMap<>(); + root.put("input_asn", asn == null ? "" : asn); + root.put("errors", errors); + engine.render(root, "asn/index.ftlh", handler); + } + + private void renderSuccess(@Nonnull TemplateEngine engine, + @Nonnull List<MailResult> sendRes, + @Nonnull Handler<AsyncResult<Buffer>> handler) { + final Map<String, Object> root = new HashMap<>(); + root.put("emails", sendRes.stream() + .filter(Objects::nonNull) // Nulls mean failures. + .flatMap(res -> res.getRecipients().stream()) + .collect(Collectors.toList())); + engine.render(root, "asn/success.ftlh", handler); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java new file mode 100644 index 0000000..4a05588 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java @@ -0,0 +1,152 @@ +package moe.yuuta.dn42peering.asn; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.templates.SqlTemplate; +import moe.yuuta.dn42peering.utils.PasswordAuthentication; +import moe.yuuta.dn42peering.whois.IWhoisService; +import moe.yuuta.dn42peering.whois.WhoisObject; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class ASNServiceImpl implements IASNService { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final Vertx vertx; + private final MySQLPool pool; + + ASNServiceImpl(@Nonnull Vertx vertx, @Nonnull MySQLPool pool) { + this.vertx = vertx; + this.pool = pool; + } + + @Nonnull + @Override + public IASNService exists(@Nonnull String asn, boolean requireActivationStatus, boolean activated, + @Nonnull Handler<AsyncResult<Boolean>> handler) { + final PreparedQuery<RowSet<Row>> preparedQuery = + !requireActivationStatus ? pool.preparedQuery("SELECT COUNT(asn) FROM asn WHERE asn = ?") + : pool.preparedQuery("SELECT COUNT(asn) FROM asn WHERE asn = ? AND activated = ?"); + preparedQuery.execute(!requireActivationStatus ? Tuple.of(asn) : Tuple.of(asn, activated)) + .compose(rows -> { + int count = rows.iterator().next().getInteger(0); + return Future.succeededFuture(count > 0); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService markAsActivated(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler) { + pool.preparedQuery("UPDATE asn SET activated = 1 WHERE asn = ? AND activated = 0") + .execute(Tuple.of(asn)) + .<Void>compose(rows -> { + return Future.succeededFuture(null); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService changePassword(@Nonnull String asn, @Nonnull String newPassword, @Nonnull Handler<AsyncResult<Void>> handler) { + final Map<String, Object> params = new HashMap<>(2); + params.put("asn", asn); + params.put("password_hash", new PasswordAuthentication() + .hash(newPassword.toCharArray())); + Future.<SqlResult<Void>>future(f -> SqlTemplate + .forUpdate(pool, "UPDATE asn SET password_hash = #{password_hash} WHERE asn = #{asn}") + .execute(params, f)) + .<Void>compose(voidSqlResult -> Future.succeededFuture(null)) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService auth(@Nonnull String asn, @Nonnull String password, @Nonnull Handler<AsyncResult<Boolean>> handler) { + Future.<RowSet<ASN>>future(f -> SqlTemplate + .forQuery(pool, "SELECT password_hash FROM asn WHERE asn = #{asn}") + .mapTo(ASNRowMapper.INSTANCE) + .mapFrom(ASNParametersMapper.INSTANCE) + .execute(new ASN(asn, "", false, new Date()), f)) + .compose(rows -> { + // No such user + if(!rows.iterator().hasNext()) { + return Future.succeededFuture(false); + } + final ASN record = rows.iterator().next(); + return Future.succeededFuture(record.auth(password)); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService delete(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler) { + Future.<SqlResult<Void>>future(f -> SqlTemplate.forUpdate(pool, "DELETE FROM asn WHERE asn = #{asn}") + .execute(Collections.singletonMap("asn", asn), f)) + .<Void>compose(voidSqlResult -> Future.succeededFuture()) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService lookupEmails(@Nonnull WhoisObject asn, @Nonnull Handler<AsyncResult<List<String>>> handler) { + if (!asn.containsKey("tech-c") || asn.get("tech-c").isEmpty()) { + handler.handle(Future.succeededFuture(null)); + return this; + } + Future.<WhoisObject>future(f -> IWhoisService.createProxy(vertx, IWhoisService.ADDRESS) + .query(asn.get("tech-c").get(0) /* tech-c is only permitted to appear once */, f)) + .compose(techLookup -> { + if(techLookup == null) { + return Future.succeededFuture(Collections.<String>emptyList()); + } else { + final List<String> verifyMethodList = new ArrayList<>(3); + verifyMethodList.addAll(Stream.of(techLookup.getOrDefault("contact", Collections.emptyList())) + .flatMap(Collection::stream) + .map(contact -> { + final Matcher m = + Pattern.compile("[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+") + .matcher(contact); + final List<String> verifiers = new ArrayList<>(1); + while (m.find()) { + verifiers.add(m.group()); + } + return verifiers; + }) + .flatMap(List::stream) + .collect(Collectors.toList())); + return Future.succeededFuture(verifyMethodList); + } + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IASNService registerOrChangePassword(@Nonnull String asn, @Nonnull String newPassword, @Nonnull Handler<AsyncResult<Void>> handler) { + // TODO: No activated check here. + Future.<RowSet<Row>>future(f -> pool.preparedQuery("REPLACE INTO asn (asn, password_hash) VALUES(?, ?)") + .execute(Tuple.of(asn, new PasswordAuthentication().hash(newPassword.toCharArray())), f)) + .<Void>compose(res -> Future.succeededFuture(null)) + .onComplete(handler); + return this; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java new file mode 100644 index 0000000..032dc2f --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java @@ -0,0 +1,56 @@ +package moe.yuuta.dn42peering.asn; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.serviceproxy.ServiceBinder; +import io.vertx.sqlclient.PoolOptions; + +public class ASNVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private MessageConsumer<JsonObject> consumer; + private MySQLPool pool; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database"); + final MySQLConnectOptions opt = new MySQLConnectOptions(json); + pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5)); + + consumer = new ServiceBinder(vertx) + .setAddress(IASNService.ADDRESS) + .register(IASNService.class, new ASNServiceImpl(vertx, pool)); + consumer.completionHandler(ar -> { + if(ar.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(ar.cause()); + } + }); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + CompositeFuture.all( + Future.future(f -> consumer.unregister(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })), + Future.future(f -> pool.close(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })) + ).onComplete(ar -> { + if(ar.succeeded()) stopPromise.complete(); + else stopPromise.fail(ar.cause()); + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java new file mode 100644 index 0000000..5da2a57 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java @@ -0,0 +1,52 @@ +package moe.yuuta.dn42peering.asn; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.ProxyGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import moe.yuuta.dn42peering.whois.WhoisObject; + +import javax.annotation.Nonnull; +import java.util.List; + +@ProxyGen +public interface IASNService { + String ADDRESS = IASNService.class.getName(); + + @Nonnull + static IASNService createProxy(@Nonnull Vertx vertx, @Nonnull String address) { + return new IASNServiceVertxEBProxy(vertx, address); + } + + @Fluent + @Nonnull + IASNService exists(@Nonnull String asn, boolean requireActivationStatus, boolean activated, + @Nonnull Handler<AsyncResult<Boolean>> handler); + + @Fluent + @Nonnull + IASNService markAsActivated(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IASNService changePassword(@Nonnull String asn, @Nonnull String newPassword, + @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IASNService registerOrChangePassword(@Nonnull String asn, @Nonnull String newPassword, + @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IASNService auth(@Nonnull String asn, @Nonnull String password, @Nonnull Handler<AsyncResult<Boolean>> handler); + + @Fluent + @Nonnull + IASNService delete(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IASNService lookupEmails(@Nonnull WhoisObject asn, @Nonnull Handler<AsyncResult<List<String>>> handler); +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java new file mode 100644 index 0000000..aa3d70a --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(groupPackage = "moe.yuuta.dn42peering.asn", name = "asn") +package moe.yuuta.dn42peering.asn; + +import io.vertx.codegen.annotations.ModuleGen;
\ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java b/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java new file mode 100644 index 0000000..37ce0cf --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java @@ -0,0 +1,22 @@ +package moe.yuuta.dn42peering.jaba; + +/** + * Oh Jesus how can't Jaba have out parameters ?! + * + * The out parameters in C# is such a beautiful innovation, + * + * as I can get rid of exceptions and write C style functions, + * + * just like <pre>int func(class *out);</pre>. + */ +public final class OutParm<E> { + public E out; + + public OutParm() { + this(null); + } + + public OutParm(E out) { + this.out = out; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java b/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java new file mode 100644 index 0000000..f0dba78 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java @@ -0,0 +1,16 @@ +package moe.yuuta.dn42peering.jaba; + +public class Pair<A, B> { + public final A a; + public final B b; + + public Pair(A a, B b) { + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return String.format("Pair { a = %s, b = %s }", a, b); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java new file mode 100644 index 0000000..7b9730d --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java @@ -0,0 +1,38 @@ +package moe.yuuta.dn42peering.manage; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.authentication.AuthenticationProvider; +import moe.yuuta.dn42peering.asn.IASNService; + +import javax.annotation.Nonnull; + +class ASNAuthProvider implements AuthenticationProvider { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final IASNService asnService; + + ASNAuthProvider(@Nonnull IASNService asnService) { + this.asnService = asnService; + } + + @Override + public void authenticate(JsonObject credentials, Handler<AsyncResult<User>> resultHandler) { + final String username = credentials.getString("username"); + final String password = credentials.getString("password"); + Future.<Boolean>future(f -> asnService.auth(username.toUpperCase(), password, f)) + .compose(succ -> { + if(succ) { + return Future.succeededFuture(User.fromName(username.toUpperCase())); + } else { + return Future.failedFuture("Incorrect logon"); + } + }) + .onComplete(resultHandler); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java new file mode 100644 index 0000000..aa04ea5 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java @@ -0,0 +1,1022 @@ +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.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.BasicAuthHandler; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.LocalSessionStore; +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 moe.yuuta.dn42peering.agent.proto.BGPRequest; +import moe.yuuta.dn42peering.agent.proto.VertxAgentGrpc; +import moe.yuuta.dn42peering.agent.proto.WGRequest; +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.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.*; + +public class ManageHandler implements ISubRouter { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + @Nonnull + @Override + @SuppressWarnings("unchecked") + public Router mount(@Nonnull Vertx vertx) { + final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS); + final IWhoisService whoisService = IWhoisService.createProxy(vertx, IWhoisService.ADDRESS); + final IPeerService peerService = IPeerService.createProxy(vertx); + final INodeService nodeService = INodeService.createProxy(vertx); + final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh"); + + final Router router = Router.router(vertx); + router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024)); + router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); + router.route().handler(BasicAuthHandler.create(new ASNAuthProvider(asnService))); + router.route().handler(ctx -> { + // Mark as activated. + asnService.markAsActivated(ctx.user().principal().getString("username"), ar -> { + if (ar.succeeded()) { + ctx.next(); + } else { + ctx.fail(ar.cause()); + } + }); + }); + + router.get("/") + .produces("text/html") + .handler(ctx -> { + 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().end(res.result()); + } else { + ctx.fail(res.cause()); + } + }); + }); + + 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().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + }); + + final ObjectSchemaBuilder registerSchema = objectSchema() + .allowAdditionalProperties(false) + .property("ipv4", stringSchema()) + .property("ipv6", stringSchema()) + .property("mpbgp", stringSchema()) + .property("vpn", enumSchema("wg")) + .property("wg_endpoint", stringSchema()) + .property("wg_endpoint_port", stringSchema()) + .property("wg_pubkey", stringSchema()) + .property("node", stringSchema()); + final SchemaParser parser = SchemaParser.createDraft7SchemaParser( + SchemaRouter.create(vertx, new SchemaRouterOptions())); + + router.post("/new") + .handler(BodyHandler.create().setBodyLimit(100 * 1024)) + .handler(ValidationHandler + .builder(parser) + .body(Bodies.formUrlEncoded(registerSchema)) + .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(); + // Parse peer + parseForm(nodeService, parameters) + .compose(peer -> { + // Keys are generated during parsing. + 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); + } + }) + ).<Peer>compose(peer -> Future.future(f -> peerService.isIPConflict(peer.getType(), + peer.getIpv4(), + peer.getIpv6(), + ar -> { + if (ar.succeeded()) { + if (ar.result()) { + f.fail(new FormException(peer, + "The IPv4 or IPv6 you specified conflicts with an existing peering with the same type.")); + } else { + f.complete(peer); + } + } else { + f.fail(ar.cause()); + } + })) + ).<Peer>compose(peer -> Future.future(f -> peerService.addNew(peer, ar -> { + if (ar.succeeded()) { + peer.setId((int) (long) ar.result()); + f.complete(peer); + } else f.fail(ar.cause()); + }))) + .onSuccess(peer -> { + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/manage") + .end(); + provisionPeer(vertx, nodeService, peer).onComplete(ar -> + this.handleProvisionResult(peerService, peer, ar)); + }) + .onFailure(err -> { + if (err instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) err).code).end(); + } else if (err instanceof FormException) { + renderForm(engine, nodeService, + true, asn, + ((Peer) ((FormException) err).data), + Arrays.asList(((FormException) err).errors), + res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + } + }); + } else { + logger.error("Cannot add peer.", 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(); + Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) + .compose(peer -> { + if (peer == null) { + return Future.failedFuture(new HTTPException(404)); + } + 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().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + }); + + router.post("/edit") + .handler(BodyHandler.create().setBodyLimit(100 * 1024)) + .handler(ValidationHandler + .builder(parser) + .body(Bodies.formUrlEncoded(registerSchema)) + .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(); + Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) + .compose(peer -> { + if (peer == null) { + return Future.failedFuture(new HTTPException(404)); + } + return Future.succeededFuture(peer); + }).compose(existingPeer -> + parseForm(nodeService, parameters) + .compose(inPeer -> { + // Preserve keys + inPeer.setWgSelfPrivKey(existingPeer.getWgSelfPrivKey()); + inPeer.setWgSelfPubkey(existingPeer.getWgSelfPubkey()); + inPeer.setWgPresharedSecret(existingPeer.getWgPresharedSecret()); + inPeer.setAsn(asn); + inPeer.setId(existingPeer.getId()); + return Future.succeededFuture(new Pair<>(existingPeer, inPeer)); + }) + ).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); + } + }); + } + ).<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; + } + final boolean nc6 = needCheckIPv6Conflict; + return Future.future(f -> peerService.isIPConflict(inPeer.getType(), + needCheckIPv4Conflict ? inPeer.getIpv4() : null, + nc6 ? inPeer.getIpv6() : null, + ar -> { + if (ar.succeeded()) { + if (ar.result()) { + f.fail(new FormException(peer, + "The IPv4 or IPv6 you specified conflicts with an existing peering with the same type.")); + } else { + f.complete(peer); + } + } else { + f.fail(ar.cause()); + } + })); + } + ).<Pair<Peer /* Existing */, Peer /* Input */>>compose(peer -> + Future.future(f -> peerService.updateTo(peer.b /* New Peer */, ar -> { + if (ar.succeeded()) f.complete(peer); + else f.fail(ar.cause()); + }))) + .onSuccess(pair -> { + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/manage") + .end(); + final Peer existingPeer = pair.a; + final Peer inPeer = pair.b; + reloadPeer(vertx, nodeService, existingPeer, inPeer).onComplete(ar -> + this.handleProvisionResult(peerService, inPeer, ar)); + }) + .onFailure(err -> { + if (err instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) err).code).end(); + } else if (err instanceof FormException) { + renderForm(engine, + nodeService, + false, + asn, + (((Pair<Peer, Peer>) ((FormException) err).data).b), + Arrays.asList(((FormException) err).errors), + res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + } + }); + } else { + logger.error("Cannot edit peer.", err); + ctx.fail(err); + } + }); + }); + + router.get("/delete") + .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(); + Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) + .compose(peer -> { + if (peer == null) { + return Future.failedFuture(new HTTPException(404)); + } + return Future.succeededFuture(peer); + }) + .compose(peer -> unprovisionPeer(vertx, nodeService, peer)) + .compose(_v -> Future.<Void>future(f -> peerService.deletePeer(asn, id, f))) + .onSuccess(_id -> ctx.response() + .setStatusCode(303) + .putHeader("Location", "/manage") + .end()) + .onFailure(err -> { + if (err instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) err).code).end(); + } else { + logger.error("Cannot delete peer.", err); + ctx.fail(err); + } + }); + }); + + router.get("/change-password") + .produces("text/html") + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + renderChangepw(engine, asn, null, res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + }); + + router.post("/change-password") + .handler(BodyHandler.create().setBodyLimit(100 * 1024)) + .handler(ValidationHandler + .builder(parser) + .body(Bodies.formUrlEncoded(objectSchema() + .property("passwd", stringSchema()) + .property("confirm", stringSchema()) + .allowAdditionalProperties(false))) + .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 passwd = parameters.getString("passwd"); + final String confirm = parameters.getString("confirm"); + Future.<Void>future(f -> { + if (passwd == null || confirm == null) { + f.fail(new FormException("Some fields are not supplied.")); + } else { + if (!passwd.equals(confirm)) { + f.fail(new FormException("Two passwords do not match.")); + } else { + f.complete(); + } + } + }).<Void>compose(v -> Future.future(f -> asnService.changePassword(asn, passwd, f))) + .onSuccess(_void -> { + ctx.session().destroy(); + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/manage") + .end(); + }) + .onFailure(err -> { + if (err instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) err).code).end(); + } else if (err instanceof FormException) { + renderChangepw(engine, asn, + Arrays.asList(((FormException) err).errors), + res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + } else { + logger.error("Cannot change password.", err); + ctx.fail(err); + } + }); + }); + + router.get("/delete-account") + .produces("text/html") + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + renderDA(engine, asn, null, res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + }); + + router.post("/delete-account") + .produces("text/html") + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + Future.<Void>future(f -> peerService.existsUnderASN(asn, ar -> { + if (ar.succeeded()) { + if (ar.result()) { + f.fail(new FormException("There are still active peers. Delete them before deleting the account.")); + } else { + f.complete(null); + } + } else { + f.fail(ar.cause()); + } + })).<Void>compose(v -> Future.future(f -> asnService.delete(asn, f))) + .onSuccess(_void -> { + ctx.session().destroy(); + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/") + .end(); + }) + .onFailure(err -> { + if (err instanceof HTTPException) { + ctx.response().setStatusCode(((HTTPException) err).code).end(); + } else if (err instanceof FormException) { + renderDA(engine, asn, + Arrays.asList(((FormException) err).errors), + res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + } else { + logger.error("Cannot delete account.", err); + ctx.fail(err); + } + }); + }); + + router.get("/show-configuration") + .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(); + Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) + .compose(peer -> { + if (peer == null) { + return Future.failedFuture(new HTTPException(404)); + } + return Future.succeededFuture(peer); + }) + .compose(peer -> renderShowConfig(nodeService, engine, peer)) + .onComplete(res -> { + if (res.succeeded()) { + ctx.response().end(res.result()); + } else { + res.cause().printStackTrace(); + ctx.fail(res.cause()); + } + }); + }); + + 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")) { + final String preIpv4 = form.getString("ipv4"); + if (preIpv4 == null || preIpv4.isEmpty()) { + errors.add("IPv4 address is not specified."); + } else { + if (InetAddressValidator.getInstance().isValidInet4Address(preIpv4)) { + if (!new CIDRUtils("172.20.0.0/14").isInRange(preIpv4)) { + errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); + } else { + ipv4 = preIpv4; + } + } 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")) { + final String preIpv6 = form.getString("ipv6"); + if (preIpv6 != null && !preIpv6.isEmpty()) { + if (InetAddressValidator.getInstance().isValidInet6Address(preIpv6)) { + if (!new CIDRUtils("fd00::/8").isInRange(preIpv6) && + !Inet6Address.getByName(preIpv6).isLinkLocalAddress()) { + errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address."); + } else { + ipv6 = preIpv6; + } + } else + errors.add("IPv6 address is illegal. Cannot parse your address."); + } + } + + 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) { + final String preIpv4 = form.getString("wg_endpoint"); + if (preIpv4 == null || preIpv4.isEmpty()) { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + } else { + if (InetAddressValidator.getInstance().isValidInet4Address(preIpv4)) { + if (new CIDRUtils("10.0.0.0/8").isInRange(preIpv4) || + new CIDRUtils("192.168.0.0/16").isInRange(preIpv4) || + new CIDRUtils("172.16.0.0/23").isInRange(preIpv4)) { + errors.add("WireGuard EndPoint is illegal. It must not be an internal address."); + } else { + wgEndpoint = preIpv4; + wgEndpointCorrect = true; + } + } else + errors.add("WireGuard EndPoint is illegal. Cannot parse your address."); + } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + } + } + + int wgEndpointPort = -1; + if (form.containsKey("wg_endpoint_port")) { + if (type == Peer.VPNType.WIREGUARD) { + if (wgEndpointCorrect) { + try { + wgEndpointPort = Integer.parseInt(form.getString("wg_endpoint_port")); + if (wgEndpointPort < 0 || wgEndpointPort > 65535) { + errors.add("WireGuard EndPoint port must be in UDP port range."); + wgEndpointPort = -1; + } + } 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, but port is specified."); + } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + } + } + + 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."); + } else { + try { + Key.fromBase64(wgPubKey); + } catch (KeyFormatException e) { + errors.add("WireGuard public key is not valid."); + wgPubKey = null; + } + } + } 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()); + + 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."); + } else { + root.put("ipv4", node.getDn42Ip4()); + root.put("ipv6", node.getDn42Ip6()); + root.put("asn", node.getAsn()); + root.put("endpoint", node.getPublicIp()); + } + + return engine.render(root, "manage/showconf.ftlh"); + }); + + } + + @Nonnull + private Future<Void> reloadPeer(@Nonnull Vertx vertx, + @Nonnull INodeService nodeService, + @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. + final boolean canReload = inPeer.getType() == existingPeer.getType() && + inPeer.getNode() == existingPeer.getNode(); + 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 -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx)); + switch (existingPeer.getType()) { + case WIREGUARD: + return stub.reloadWG( + inPeer.toWGRequest().setNode(node.toRPCNode()).build() + ).compose(wgReply -> Future.succeededFuture(new Pair<>(node, wgReply.getDevice()))); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(pair -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(pair.a.toChannel(vertx)); + return stub.reloadBGP(inPeer.toBGPRequest() + .setNode(pair.a.toRPCNode()) + .setDevice(pair.b) + .build()) + .compose(reply -> Future.succeededFuture(null)); + }); + } else { + future = unprovisionPeer(vertx, nodeService, existingPeer) + .compose(f -> provisionPeer(vertx, nodeService, inPeer)); + } + return future; + } + + private Future<Void> unprovisionPeer(@Nonnull Vertx vertx, + @Nonnull INodeService nodeService, + @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 -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx)); + switch (existingPeer.getType()) { + case WIREGUARD: + return stub.deleteWG(WGRequest.newBuilder().setId(existingPeer.getId()).build()) + .compose(wgReply -> Future.succeededFuture(node)); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(node -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx)); + return stub.deleteBGP(BGPRequest.newBuilder().setId(existingPeer.getId()) + .build()) + .compose(reply -> Future.succeededFuture(null)); + }) + ; + } + + @Nonnull + private Future<Void> provisionPeer(@Nonnull Vertx vertx, + @Nonnull INodeService nodeService, + @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 -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx)); + switch (inPeer.getType()) { + case WIREGUARD: + return stub.provisionWG( + inPeer.toWGRequest().setNode(node.toRPCNode()).build() + ).compose(wgReply -> Future.succeededFuture(new Pair<>(node, wgReply.getDevice()))); + default: + throw new UnsupportedOperationException("Bug: Unknown type."); + } + }) + .compose(pair -> { + final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(pair.a.toChannel(vertx)); + return stub.provisionBGP(inPeer.toBGPRequest() + .setNode(pair.a.toRPCNode()) + .setDevice(pair.b) + .build()) + .compose(reply -> Future.succeededFuture(null)); + }); + } + + 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/node/INodeService.java b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java new file mode 100644 index 0000000..9323faa --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java @@ -0,0 +1,28 @@ +package moe.yuuta.dn42peering.node; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.ProxyGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +import javax.annotation.Nonnull; +import java.util.List; + +@ProxyGen +public interface INodeService { + String ADDRESS = INodeService.class.getName(); + + @Nonnull + static INodeService createProxy(@Nonnull Vertx vertx) { + return new INodeServiceVertxEBProxy(vertx, ADDRESS); + } + + @Fluent + @Nonnull + INodeService listNodes(@Nonnull Handler<AsyncResult<List<Node>>> handler); + + @Fluent + @Nonnull + INodeService getNode(int id, @Nonnull Handler<AsyncResult<Node>> handler); +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/Node.java b/central/src/main/java/moe/yuuta/dn42peering/node/Node.java new file mode 100644 index 0000000..31c0be2 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/node/Node.java @@ -0,0 +1,204 @@ +package moe.yuuta.dn42peering.node; + +import io.grpc.ManagedChannel; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.format.SnakeCase; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.grpc.VertxChannelBuilder; +import io.vertx.sqlclient.templates.annotations.Column; +import io.vertx.sqlclient.templates.annotations.ParametersMapped; +import io.vertx.sqlclient.templates.annotations.RowMapped; +import io.vertx.sqlclient.templates.annotations.TemplateParameter; +import moe.yuuta.dn42peering.peer.Peer; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +@DataObject +@RowMapped(formatter = SnakeCase.class) +@ParametersMapped(formatter = SnakeCase.class) +public class Node { + @Column(name = "id") + @TemplateParameter(name = "id") + private int id; + + @Column(name = "public_ip") + @TemplateParameter(name = "public_ip") + private String publicIp; + + @Column(name = "dn42_ip4") + @TemplateParameter(name = "dn42_ip4") + private String dn42Ip4; + + @Column(name = "dn42_ip6") + @TemplateParameter(name = "dn42_ip6") + private String dn42Ip6; + + @Column(name = "asn") + @TemplateParameter(name = "asn") + private String asn; + + @Column(name = "internal_ip") + @TemplateParameter(name = "internal_ip") + private String internalIp; + + @Column(name = "internal_port") + @TemplateParameter(name = "internal_port") + private int internalPort; + + @Column(name = "name") + @TemplateParameter(name = "name") + private String name; + + @Column(name = "notice") + @TemplateParameter(name = "notice") + private String notice; + + @Column(name = "vpn_type_wg") + @TemplateParameter(name = "vpn_type_wg") + private boolean wireguard; + + public Node() {} + + public Node(@Nonnull JsonObject object) { + this.id = object.getInteger("id"); + this.publicIp = object.getString("public_ip"); + this.dn42Ip4 = object.getString("dn42_ip4"); + this.dn42Ip6 = object.getString("dn42_ip6"); + this.asn = object.getString("asn"); + this.internalIp = object.getString("internal_ip"); + this.internalPort = object.getInteger("internal_port"); + this.name = object.getString("name"); + this.notice = object.getString("notice"); + this.wireguard = object.getBoolean("wireguard"); + } + + @Nonnull + public JsonObject toJson() { + return new JsonObject() + .put("id", id) + .put("public_ip", publicIp) + .put("dn42_ip4", dn42Ip4) + .put("dn42_ip6", dn42Ip6) + .put("asn", asn) + .put("internal_ip", internalIp) + .put("internal_port", internalPort) + .put("name", name) + .put("notice", notice) + .put("wireguard", wireguard); + } + + @GenIgnore + @Nonnull + public List<Peer.VPNType> getSupportedVPNTypes() { + final List<Peer.VPNType> vpnTypes = new ArrayList<>(1); + if(wireguard) vpnTypes.add(Peer.VPNType.WIREGUARD); + return vpnTypes; + } + + @GenIgnore + @Nonnull + public moe.yuuta.dn42peering.agent.proto.Node toRPCNode() { + return moe.yuuta.dn42peering.agent.proto.Node.newBuilder() + .setId(id) + .setIpv4(dn42Ip4) + .setIpv6(dn42Ip6) + .build(); + } + + @GenIgnore + @Nonnull + public ManagedChannel toChannel(@Nonnull Vertx vertx) { + return VertxChannelBuilder.forAddress(vertx, internalIp, internalPort) + .usePlaintext() + .build(); + } + + // BEGIN GETTER / SETTER + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getPublicIp() { + return publicIp; + } + + public void setPublicIp(String publicIp) { + this.publicIp = publicIp; + } + + public String getDn42Ip4() { + return dn42Ip4; + } + + public void setDn42Ip4(String dn42Ip4) { + this.dn42Ip4 = dn42Ip4; + } + + public String getDn42Ip6() { + return dn42Ip6; + } + + public void setDn42Ip6(String dn42Ip6) { + this.dn42Ip6 = dn42Ip6; + } + + public String getAsn() { + return asn; + } + + public void setAsn(String asn) { + this.asn = asn; + } + + public String getInternalIp() { + return internalIp; + } + + public void setInternalIp(String internalIp) { + this.internalIp = internalIp; + } + + public int getInternalPort() { + return internalPort; + } + + public void setInternalPort(int internalPort) { + this.internalPort = internalPort; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNotice() { + return notice; + } + + public void setNotice(String notice) { + this.notice = notice; + } + + public boolean isWireguard() { + return wireguard; + } + + public void setWireguard(boolean wireguard) { + this.wireguard = wireguard; + } + + + // END GETTER / SETTER +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java new file mode 100644 index 0000000..37d3036 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java @@ -0,0 +1,64 @@ +package moe.yuuta.dn42peering.node; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.templates.SqlTemplate; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class NodeServiceImpl implements INodeService { + private final MySQLPool pool; + private final Vertx vertx; + + NodeServiceImpl(@Nonnull Vertx vertx, @Nonnull MySQLPool mySQLPool) { + this.vertx = vertx; + this.pool = mySQLPool; + } + + @Nonnull + @Override + public INodeService listNodes(@Nonnull Handler<AsyncResult<List<Node>>> handler) { + SqlTemplate + .forQuery(pool, "SELECT id, public_ip, dn42_ip4, dn42_ip6, asn, " + + "internal_ip, internal_port, name, notice, vpn_type_wg " + + "FROM node") + .mapTo(NodeRowMapper.INSTANCE) + .execute(null) + .compose(nodeRowMappers -> { + final List<Node> nodes = new ArrayList<>(10); + for (Node node : nodeRowMappers) + nodes.add(node); + return Future.succeededFuture(nodes); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public INodeService getNode(int id, @Nonnull Handler<AsyncResult<Node>> handler) { + SqlTemplate + .forQuery(pool, "SELECT id, public_ip, asn, " + + "dn42_ip4, dn42_ip6, " + + "internal_ip, internal_port, name, notice, vpn_type_wg " + + "FROM node " + + "WHERE id = #{id}") + .mapTo(NodeRowMapper.INSTANCE) + .execute(Collections.singletonMap("id", id)) + .compose(nodeRowMappers -> { + if(nodeRowMappers.iterator().hasNext()) { + return Future.succeededFuture(nodeRowMappers.iterator().next()); + } else { + return Future.succeededFuture(null); + } + }) + .onComplete(handler); + return this; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java new file mode 100644 index 0000000..3607e08 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java @@ -0,0 +1,56 @@ +package moe.yuuta.dn42peering.node; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.serviceproxy.ServiceBinder; +import io.vertx.sqlclient.PoolOptions; + +public class NodeVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private MessageConsumer<JsonObject> consumer; + private MySQLPool pool; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database"); + final MySQLConnectOptions opt = new MySQLConnectOptions(json); + pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5)); + + consumer = new ServiceBinder(vertx) + .setAddress(INodeService.ADDRESS) + .register(INodeService.class, new NodeServiceImpl(vertx, pool)); + consumer.completionHandler(ar -> { + if(ar.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(ar.cause()); + } + }); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + CompositeFuture.all( + Future.future(f -> consumer.unregister(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })), + Future.future(f -> pool.close(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })) + ).onComplete(ar -> { + if(ar.succeeded()) stopPromise.complete(); + else stopPromise.fail(ar.cause()); + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java new file mode 100644 index 0000000..c4e6eaf --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(groupPackage = "moe.yuuta.dn42peering.node", name = "node") +package moe.yuuta.dn42peering.node; + +import io.vertx.codegen.annotations.ModuleGen;
\ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java new file mode 100644 index 0000000..5f4a7af --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java @@ -0,0 +1,53 @@ +package moe.yuuta.dn42peering.peer; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.ProxyGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +@ProxyGen +public interface IPeerService { + String ADDRESS = IPeerService.class.getName(); + + @Nonnull + static IPeerService createProxy(@Nonnull Vertx vertx) { + return new IPeerServiceVertxEBProxy(vertx, ADDRESS); + } + + @Fluent + @Nonnull + IPeerService listUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<List<Peer>>> handler); + + @Fluent + @Nonnull + IPeerService getSingle(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Peer>> handler); + + @Fluent + @Nonnull + IPeerService deletePeer(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IPeerService updateTo(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Void>> handler); + + @Fluent + @Nonnull + IPeerService addNew(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Long>> handler); + + @Fluent + @Nonnull + IPeerService existsUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<Boolean>> handler); + + @Fluent + @Nonnull + IPeerService isIPConflict(@Nonnull Peer.VPNType type, @Nullable String ipv4, @Nullable String ipv6, @Nonnull Handler<AsyncResult<Boolean>> handler); + + @Fluent + @Nonnull + IPeerService changeProvisionStatus(int id, @Nonnull ProvisionStatus provisionStatus, @Nonnull Handler<AsyncResult<Void>> handler); +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java b/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java new file mode 100644 index 0000000..31560ad --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java @@ -0,0 +1,333 @@ +package moe.yuuta.dn42peering.peer; + +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.format.SnakeCase; +import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.templates.annotations.Column; +import io.vertx.sqlclient.templates.annotations.ParametersMapped; +import io.vertx.sqlclient.templates.annotations.RowMapped; +import io.vertx.sqlclient.templates.annotations.TemplateParameter; +import moe.yuuta.dn42peering.agent.proto.BGPRequest; +import moe.yuuta.dn42peering.agent.proto.WGRequest; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.net.Inet6Address; + +// DB Table: peer +@DataObject(jsonPropertyNameFormatter = SnakeCase.class) +@RowMapped(formatter = SnakeCase.class) +@ParametersMapped(formatter = SnakeCase.class) +public class Peer { + public enum VPNType { + WIREGUARD + } + + @Column(name = "id") + @TemplateParameter(name = "id") + private int id; + + @Column(name = "type") + @TemplateParameter(name = "type") + private VPNType type; + + @Column(name = "asn") + @TemplateParameter(name = "asn") + private String asn; + + @Column(name = "ipv4") + @TemplateParameter(name = "ipv4") + private String ipv4; + + @Column(name = "ipv6") + @TemplateParameter(name = "ipv6") + private String ipv6; + + @Column(name = "wg_endpoint") + @TemplateParameter(name = "wg_endpoint") + private String wgEndpoint; + + @Column(name = "wg_endpoint_port") + @TemplateParameter(name = "wg_endpoint_port") + private int wgEndpointPort; + + @Column(name = "wg_self_pubkey") + @TemplateParameter(name = "wg_self_pubkey") + private String wgSelfPubkey; + + @Column(name = "wg_self_privkey") + @TemplateParameter(name = "wg_self_privkey") + private String wgSelfPrivKey; + + @Column(name = "wg_peer_pubkey") + @TemplateParameter(name = "wg_peer_pubkey") + private String wgPeerPubkey; + + @Column(name = "wg_preshared_secret") + @TemplateParameter(name = "wg_preshared_secret") + private String wgPresharedSecret; + + @Column(name = "provision_status") + @TemplateParameter(name = "provision_status") + private ProvisionStatus provisionStatus; + + @Column(name = "mpbgp") + @TemplateParameter(name = "mpbgp") + private boolean mpbgp; + + @Column(name = "node") + @TemplateParameter(name = "node") + private int node; + + public Peer() {} + + public Peer(int id, + VPNType type, + String asn, + String ipv4, + String ipv6, + String wgEndpoint, + int wgEndpointPort, + String wgSelfPubkey, + String wgSelfPrivKey, + String wgPeerPubkey, + String wgPresharedSecret, + ProvisionStatus provisionStatus, + boolean mpbgp, + int node) { + this.id = id; + this.type = type; + this.asn = asn; + this.ipv4 = ipv4; + this.ipv6 = ipv6; + this.wgEndpoint = wgEndpoint; + this.wgEndpointPort = wgEndpointPort; + this.wgSelfPubkey = wgSelfPubkey; + this.wgSelfPrivKey = wgSelfPrivKey; + this.wgPeerPubkey = wgPeerPubkey; + this.wgPresharedSecret = wgPresharedSecret; + this.provisionStatus = provisionStatus; + this.mpbgp = mpbgp; + this.node = node; + } + + public Peer(@Nonnull JsonObject jsonObject) { + this(jsonObject.getInteger("id"), + jsonObject.getString("type") == null ? null : VPNType.valueOf(jsonObject.getString("type")), + jsonObject.getString("asn"), + jsonObject.getString("ipv4"), + jsonObject.getString("ipv6"), + jsonObject.getString("wg_endpoint"), + jsonObject.getInteger("wg_endpoint_port"), + jsonObject.getString("wg_self_pubkey"), + jsonObject.getString("wg_self_privkey"), + jsonObject.getString("wg_peer_pubkey"), + jsonObject.getString("wg_preshared_secret"), + jsonObject.getString("provision_status") == null ? null : + ProvisionStatus.valueOf(jsonObject.getString("provision_status")), + jsonObject.getBoolean("mpbgp"), + jsonObject.getInteger("node")); + } + + /** + * Create an instance with unfinished new WireGuard peering. + * Keys are generated. ASN is not filled in. ID is set to -1. ProvisionStatus is set to not provisioned. + */ + public Peer(String ipv4, + String ipv6, + String wgEndpoint, + int wgEndpointPort, + String wgPeerPubkey, + boolean mpbgp, + int node) { + this(-1, + VPNType.WIREGUARD, + null /* ASN: To be filled later */, + ipv4, + ipv6, + wgEndpoint, + wgEndpointPort, + null /* Public key: generate later */, + Key.generatePrivateKey().toBase64(), + wgPeerPubkey, + Key.generatePrivateKey().toBase64(), + ProvisionStatus.NOT_PROVISIONED, + mpbgp, + node); + try { + wgSelfPubkey = Key.generatePublicKey(Key.fromBase64(wgSelfPrivKey)).toBase64(); + } catch (KeyFormatException e) { /* Should not happen */ + throw new RuntimeException(e); } + } + + @Nonnull + public JsonObject toJson() { + return new JsonObject() + .put("id", id) + .put("type", type) + .put("asn", asn) + .put("ipv4", ipv4) + .put("ipv6", ipv6) + .put("wg_endpoint", wgEndpoint) + .put("wg_endpoint_port", wgEndpointPort) + .put("wg_self_pubkey", wgSelfPubkey) + .put("wg_self_privkey", wgSelfPrivKey) + .put("wg_peer_pubkey", wgPeerPubkey) + .put("wg_preshared_secret", wgPresharedSecret) + .put("provision_status", provisionStatus) + .put("mpbgp", mpbgp) + .put("node", node); + } + + @Nonnull + public String calcWireGuardPort() { + return asn.substring(asn.length() - 5); + } + + @GenIgnore + public boolean isIPv6LinkLocal() throws IOException { + return Inet6Address.getByName(ipv6).isLinkLocalAddress(); + } + + @GenIgnore + public WGRequest.Builder toWGRequest() { + return WGRequest.newBuilder() + .setId(getId()) + .setListenPort(Integer.parseInt(calcWireGuardPort())) + .setEndpoint(String.format("%s:%d", getWgEndpoint(), getWgEndpointPort())) + .setPeerPubKey(getWgPeerPubkey()) + .setSelfPrivKey(getWgSelfPrivKey()) + .setSelfPresharedSecret(getWgPresharedSecret()) + .setPeerIPv4(getIpv4()) + .setPeerIPv6(getIpv6() == null ? "" : getIpv6()); + } + + @GenIgnore + public BGPRequest.Builder toBGPRequest() { + return BGPRequest.newBuilder() + .setId(getId()) + .setAsn(getAsn()) + .setIpv4(getIpv4()) + .setIpv6(getIpv6() == null ? "" : getIpv6()) + .setMpbgp(isMpbgp()); + } + + // START GETTERS / SETTERS + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public VPNType getType() { + return type; + } + + public void setType(VPNType type) { + this.type = type; + } + + public String getAsn() { + return asn; + } + + public void setAsn(String asn) { + this.asn = asn; + } + + public String getIpv4() { + return ipv4; + } + + public void setIpv4(String ipv4) { + this.ipv4 = ipv4; + } + + public String getIpv6() { + return ipv6; + } + + public void setIpv6(String ipv6) { + this.ipv6 = ipv6; + } + + public String getWgEndpoint() { + return wgEndpoint; + } + + public void setWgEndpoint(String wgEndpoint) { + this.wgEndpoint = wgEndpoint; + } + + public int getWgEndpointPort() { + return wgEndpointPort; + } + + public void setWgEndpointPort(int wgEndpointPort) { + this.wgEndpointPort = wgEndpointPort; + } + + public String getWgSelfPubkey() { + return wgSelfPubkey; + } + + public void setWgSelfPubkey(String wgSelfPubkey) { + this.wgSelfPubkey = wgSelfPubkey; + } + + public String getWgSelfPrivKey() { + return wgSelfPrivKey; + } + + public void setWgSelfPrivKey(String wgSelfPrivKey) { + this.wgSelfPrivKey = wgSelfPrivKey; + } + + public String getWgPeerPubkey() { + return wgPeerPubkey; + } + + public void setWgPeerPubkey(String wgPeerPubkey) { + this.wgPeerPubkey = wgPeerPubkey; + } + + public String getWgPresharedSecret() { + return wgPresharedSecret; + } + + public void setWgPresharedSecret(String wgPresharedSecret) { + this.wgPresharedSecret = wgPresharedSecret; + } + + public ProvisionStatus getProvisionStatus() { + return provisionStatus; + } + + public void setProvisionStatus(ProvisionStatus provisionStatus) { + this.provisionStatus = provisionStatus; + } + + public boolean isMpbgp() { + return mpbgp; + } + + public void setMpbgp(boolean mpbgp) { + this.mpbgp = mpbgp; + } + + public int getNode() { + return node; + } + + public void setNode(int node) { + this.node = node; + } + + // END GETTERS / SETTERS +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java new file mode 100644 index 0000000..99c31af --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java @@ -0,0 +1,194 @@ +package moe.yuuta.dn42peering.peer; + +import io.vertx.core.*; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.mysqlclient.MySQLClient; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.SqlClient; +import io.vertx.sqlclient.SqlResult; +import io.vertx.sqlclient.templates.SqlTemplate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +class PeerServiceImpl implements IPeerService { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final Vertx vertx; + private final SqlClient pool; + + PeerServiceImpl(@Nonnull Vertx vertx, @Nonnull SqlClient sql) { + this.vertx = vertx; + this.pool = sql; + } + + @Nonnull + @Override + public IPeerService listUnderASN(@Nonnull String asn, @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 asn = #{asn}") + .mapTo(PeerRowMapper.INSTANCE) + .execute(Collections.singletonMap("asn", asn)) + .compose(peers -> { + final List<Peer> peerList = new ArrayList<>(); + for (Peer peer : peers) peerList.add(peer); + return Future.succeededFuture(peerList); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService existsUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<Boolean>> handler) { + SqlTemplate + .forQuery(pool, "SELECT COUNT(id) FROM peer " + + "WHERE asn = #{asn}") + .execute(Collections.singletonMap("asn", asn)) + .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0)) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService updateTo(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Void>> handler) { + SqlTemplate + .forUpdate(pool, "UPDATE peer SET " + + "type = #{type}, " + + "ipv4 = #{ipv4}, " + + "ipv6 = #{ipv6}, " + + "wg_endpoint = #{wg_endpoint}, " + + "wg_endpoint_port = #{wg_endpoint_port}, " + + "wg_self_pubkey = #{wg_self_pubkey}, " + + "wg_self_privkey = #{wg_self_privkey}, " + + "wg_peer_pubkey = #{wg_peer_pubkey}, " + + "wg_preshared_secret = #{wg_preshared_secret}, " + + "provision_status = #{provision_status}, " + + "mpbgp = #{mpbgp}, " + + "node = #{node} " + + "WHERE id = #{id} AND asn = #{asn}") + .mapFrom(PeerParametersMapper.INSTANCE) + .execute(peer) + .<Void>compose(res -> Future.succeededFuture(null)) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService addNew(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Long>> handler) { + peer.setId(0); + Future.<RowSet<Peer>>future(f -> SqlTemplate + .forQuery(pool, "INSERT INTO peer VALUES (#{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}" + + ")") + .mapFrom(PeerParametersMapper.INSTANCE) + .mapTo(PeerRowMapper.INSTANCE) + .execute(peer, f)) + .compose(rows -> Future.succeededFuture(rows.property(MySQLClient.LAST_INSERTED_ID))) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService isIPConflict(@Nonnull Peer.VPNType type, @Nullable String ipv4, @Nullable String ipv6, @Nonnull Handler<AsyncResult<Boolean>> handler) { + final List<Future> futures = new ArrayList<>(2); + if (ipv4 != null) { + final Map<String, Object> params = new HashMap<>(3); + params.put("type", type.toString()); + params.put("ip", ipv4); + + futures.add(Future.<RowSet<Row>>future(f -> SqlTemplate + .forQuery(pool, "SELECT COUNT(id) FROM peer WHERE type = #{type} AND ipv4 = #{ip}") + .execute(params, f)) + .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0))); + } + if (ipv6 != null) { + final Map<String, Object> params = new HashMap<>(3); + params.put("type", type.toString()); + params.put("ip", ipv6); + + futures.add(Future.<RowSet<Row>>future(f -> SqlTemplate + .forQuery(pool, "SELECT COUNT(id) FROM peer WHERE type = #{type} AND ipv6 = #{ip}") + .execute(params, f)) + .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0))); + } + if(futures.isEmpty()) { + Future.succeededFuture(false).onComplete(handler); + return this; + } + CompositeFuture.all(futures) + .compose(future -> { + final List<Boolean> res = future.list(); + for (boolean b : res) { + if (b) return Future.succeededFuture(true); + } + return Future.succeededFuture(false); + }) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService getSingle(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Peer>> handler) { + final Map<String, Object> params = new HashMap<>(2); + params.put("id", id); + params.put("asn", asn); + Future.<RowSet<Peer>>future(f -> 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 id = #{id} AND asn = #{asn}") + .mapTo(PeerRowMapper.INSTANCE) + .execute(params, f)) + .compose(peers -> Future.succeededFuture(peers.iterator().next())) + .onComplete(handler); + return this; + } + + @Nonnull + @Override + public IPeerService deletePeer(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Void>> handler) { + final Map<String, Object> params = new HashMap<>(2); + params.put("id", id); + params.put("asn", asn); + 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); + return this; + } + + @Nonnull + @Override + public IPeerService changeProvisionStatus(int id, @Nonnull ProvisionStatus provisionStatus, @Nonnull Handler<AsyncResult<Void>> handler) { + final Map<String, Object> params = new HashMap<>(2); + params.put("id", id); + params.put("provision_status", provisionStatus); + SqlTemplate + .forUpdate(pool, "UPDATE peer SET provision_status = #{provision_status} WHERE id = #{id}") + .execute(params) + .compose(res -> Future.<Void>succeededFuture(null)) + .onComplete(handler); + return this; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java new file mode 100644 index 0000000..d13dfec --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java @@ -0,0 +1,56 @@ +package moe.yuuta.dn42peering.peer; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.serviceproxy.ServiceBinder; +import io.vertx.sqlclient.PoolOptions; + +public class PeerVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private MessageConsumer<JsonObject> consumer; + private MySQLPool pool; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database"); + final MySQLConnectOptions opt = new MySQLConnectOptions(json); + pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5)); + + consumer = new ServiceBinder(vertx) + .setAddress(IPeerService.ADDRESS) + .register(IPeerService.class, new PeerServiceImpl(vertx, pool)); + consumer.completionHandler(ar -> { + if(ar.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(ar.cause()); + } + }); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + CompositeFuture.all( + Future.future(f -> consumer.unregister(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })), + Future.future(f -> pool.close(ar -> { + if(ar.succeeded()) f.complete(); + else f.fail(ar.cause()); + })) + ).onComplete(ar -> { + if(ar.succeeded()) stopPromise.complete(); + else stopPromise.fail(ar.cause()); + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java b/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java new file mode 100644 index 0000000..d7afbb4 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java @@ -0,0 +1,7 @@ +package moe.yuuta.dn42peering.peer; + +public enum ProvisionStatus { + PROVISIONED, + NOT_PROVISIONED, + FAIL +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java new file mode 100644 index 0000000..4bf2c3e --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(groupPackage = "moe.yuuta.dn42peering.peer", name = "peer") +package moe.yuuta.dn42peering.peer; + +import io.vertx.codegen.annotations.ModuleGen;
\ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java b/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java new file mode 100644 index 0000000..f1e7663 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java @@ -0,0 +1,17 @@ +package moe.yuuta.dn42peering.portal; + +import javax.annotation.Nonnull; + +public class FormException extends Exception { + public String[] errors; + public final Object data; + + public FormException(@Nonnull String... errors) { + this(null, errors); + } + + public FormException(Object data, @Nonnull String... errors) { + this.errors = errors; + this.data = data; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java new file mode 100644 index 0000000..9faadb5 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java @@ -0,0 +1,9 @@ +package moe.yuuta.dn42peering.portal; + +public class HTTPException extends Exception { + public final int code; + + public HTTPException(int code) { + this.code = code; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java new file mode 100644 index 0000000..9ce34ba --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java @@ -0,0 +1,51 @@ +package moe.yuuta.dn42peering.portal; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +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.templ.freemarker.FreeMarkerTemplateEngine; +import moe.yuuta.dn42peering.asn.ASNHandler; +import moe.yuuta.dn42peering.manage.ManageHandler; + +public class HTTPPortalVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + @Override + public void start(Promise<Void> startPromise) throws Exception { + final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh"); + + final Router router = Router.router(vertx); + router.get("/") + .produces("text/html") + .handler(ctx -> { + final JsonObject data = new JsonObject() + .put("name", config().getJsonObject("http").getValue("name")); + engine.render(data, "index.ftlh", res -> { + if(res.succeeded()) { + ctx.response().end(res.result()); + } else { + ctx.fail(res.cause()); + logger.error("Cannot render index.", res.cause()); + } + }); + }); + router.mountSubRouter("/asn", new ASNHandler().mount(vertx)); + router.mountSubRouter("/manage", new ManageHandler().mount(vertx)); + router.errorHandler(500, ctx -> { + logger.error("Generic Error", ctx.failure()); + }); + vertx.createHttpServer() + .requestHandler(router) + .listen(8080, res -> { + if(res.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(res.cause()); + } + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java b/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java new file mode 100644 index 0000000..d75d041 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java @@ -0,0 +1,10 @@ +package moe.yuuta.dn42peering.portal; + +import io.vertx.core.Vertx; +import io.vertx.ext.web.Router; + +import javax.annotation.Nonnull; + +public interface ISubRouter { + @Nonnull Router mount(@Nonnull Vertx vertx); +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java b/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java new file mode 100644 index 0000000..2934b31 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java @@ -0,0 +1,139 @@ +package moe.yuuta.dn42peering.utils; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Hash passwords for storage, and test passwords against password tokens. + * + * Instances of this class can be used concurrently by multiple threads. + * + * @author erickson + * @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a> + */ +public final class PasswordAuthentication { + + /** + * Each token produced by this class uses this identifier as a prefix. + */ + public static final String ID = "$31$"; + + /** + * The minimum recommended cost, used by default + */ + public static final int DEFAULT_COST = 16; + + private static final String ALGORITHM = "PBKDF2WithHmacSHA1"; + + private static final int SIZE = 128; + + private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})"); + + private final SecureRandom random; + + private final int cost; + + public PasswordAuthentication() { + this(DEFAULT_COST); + } + + /** + * Create a password manager with a specified cost + * + * @param cost the exponential computational cost of hashing a password, 0 to 30 + */ + public PasswordAuthentication(int cost) { + iterations(cost); /* Validate cost */ + this.cost = cost; + this.random = new SecureRandom(); + } + + private static int iterations(int cost) { + if ((cost < 0) || (cost > 30)) + throw new IllegalArgumentException("cost: " + cost); + return 1 << cost; + } + + /** + * Hash a password for storage. + * + * @return a secure authentication token to be stored for later authentication + */ + public String hash(char[] password) { + byte[] salt = new byte[SIZE / 8]; + random.nextBytes(salt); + byte[] dk = pbkdf2(password, salt, 1 << cost); + byte[] hash = new byte[salt.length + dk.length]; + System.arraycopy(salt, 0, hash, 0, salt.length); + System.arraycopy(dk, 0, hash, salt.length, dk.length); + Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding(); + return ID + cost + '$' + enc.encodeToString(hash); + } + + /** + * Authenticate with a password and a stored password token. + * + * @return true if the password and token match + */ + public boolean authenticate(char[] password, String token) { + Matcher m = layout.matcher(token); + if (!m.matches()) + throw new IllegalArgumentException("Invalid token format"); + int iterations = iterations(Integer.parseInt(m.group(1))); + byte[] hash = Base64.getUrlDecoder().decode(m.group(2)); + byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8); + byte[] check = pbkdf2(password, salt, iterations); + int zero = 0; + for (int idx = 0; idx < check.length; ++idx) + zero |= hash[salt.length + idx] ^ check[idx]; + return zero == 0; + } + + private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE); + try { + SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM); + return f.generateSecret(spec).getEncoded(); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex); + } + catch (InvalidKeySpecException ex) { + throw new IllegalStateException("Invalid SecretKeyFactory", ex); + } + } + + /** + * Hash a password in an immutable {@code String}. + * + * <p>Passwords should be stored in a {@code char[]} so that it can be filled + * with zeros after use instead of lingering on the heap and elsewhere. + * + * @deprecated Use {@link #hash(char[])} instead + */ + @Deprecated + public String hash(String password) { + return hash(password.toCharArray()); + } + + /** + * Authenticate with a password in an immutable {@code String} and a stored + * password token. + * + * @deprecated Use {@link #authenticate(char[],String)} instead. + * @see #hash(String) + */ + @Deprecated + public boolean authenticate(String password, String token) { + return authenticate(password.toCharArray(), token); + } + +}
\ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java b/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java new file mode 100644 index 0000000..0b773bf --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java @@ -0,0 +1,27 @@ +package moe.yuuta.dn42peering.whois; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.ProxyGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + +import javax.annotation.Nonnull; + +@ProxyGen +public interface IWhoisService { + String ADDRESS = IWhoisService.class.getName(); + + static IWhoisService create(@Nonnull Vertx vertx) { + return new WhoisServiceImpl(vertx); + } + + @Nonnull + static IWhoisService createProxy(@Nonnull Vertx vertx, @Nonnull String address) { + return new IWhoisServiceVertxEBProxy(vertx, address); + } + + @Fluent + @Nonnull + IWhoisService query(@Nonnull String handle, @Nonnull Handler<AsyncResult<WhoisObject>> handler); +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java new file mode 100644 index 0000000..37ad75d --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java @@ -0,0 +1,56 @@ +package moe.yuuta.dn42peering.whois; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@DataObject(generateConverter = true) +public class WhoisObject { + private Map<String, JsonArray> map; + + public WhoisObject(JsonObject jsonObject) { + map = new HashMap<>(jsonObject.size()); + jsonObject.stream() + .forEach(entity -> { + map.put(entity.getKey(), (JsonArray) entity.getValue()); + }); + } + + public WhoisObject(@Nonnull Map<String, List<String>> map) { + this.map = new HashMap<>(map.size()); + map.keySet().forEach(key -> { + final JsonArray array = new JsonArray(); + map.get(key).forEach(array::add); + this.map.put(key, array); + }); + } + + @Nonnull + public JsonObject toJson() { + final JsonObject jsonObject = new JsonObject(); + map.keySet() + .forEach(key -> jsonObject.put(key, map.get(key))); + return jsonObject; + } + + public boolean containsKey(@Nonnull String key) { + return map.containsKey(key); + } + + public List<String> getOrDefault(@Nonnull String key, List<String> def) { + if(containsKey(key)) return map.get(key).stream().map(obj -> (String)obj).collect(Collectors.toList()); + return def; + } + + @Nonnull + public List<String> get(@Nonnull String key) { + return getOrDefault(key, Collections.emptyList()); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java new file mode 100644 index 0000000..57b167e --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java @@ -0,0 +1,71 @@ +package moe.yuuta.dn42peering.whois; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import moe.yuuta.dn42peering.jaba.OutParm; +import org.apache.commons.net.whois.WhoisClient; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class WhoisServiceImpl implements IWhoisService { + private final Vertx vertx; + + WhoisServiceImpl(@Nonnull Vertx vertx) { + this.vertx = vertx; + } + + @Nonnull + @Override + public IWhoisService query(@Nonnull String handle, @Nonnull Handler<AsyncResult<WhoisObject>> handler) { + final WhoisClient whoisClient = new WhoisClient(); + vertx.<String>executeBlocking(f -> { + try { + whoisClient.connect(vertx.getOrCreateContext().config().getString("whois")); + final String result = whoisClient.query(handle); + whoisClient.disconnect(); + f.complete(result); + } catch (IOException e) { + f.fail(e); + } + }).compose(rawOutput -> { + if(rawOutput == null) return Future.succeededFuture(null); + else return Future.succeededFuture(parseObject(rawOutput)); + }).onComplete(handler); + return this; + } + + @Nullable + private static WhoisObject parseObject(@Nonnull String s) { + final Map<String, List<String>> obj = new HashMap<>(5); + for(final String line : s.split("\n")) { + final OutParm<String> key = new OutParm<>(); + final OutParm<String> value = new OutParm<>(); + if(!parseLine(line, key, value)) { + continue; + } + final List<String> values = obj.containsKey(key.out) ? obj.get(key.out) : new ArrayList<>(1); + values.add(value.out); + obj.put(key.out, values); + } + if(obj.isEmpty()) return null; + return new WhoisObject(obj); + } + + /* Testing only */ static boolean parseLine(@Nonnull String line, @Nonnull OutParm<String> key, @Nonnull OutParm<String> value) { + if(line.startsWith("#") || line.startsWith("%")) return false; + if(line.length() < 20) return false; + final String part1 = line.substring(0, 20); + final String part2 = line.substring(20); + key.out = part1.split(":")[0]; + value.out = part2; + return true; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java new file mode 100644 index 0000000..d4cc83a --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java @@ -0,0 +1,35 @@ +package moe.yuuta.dn42peering.whois; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.serviceproxy.ServiceBinder; + +public class WhoisVerticle extends AbstractVerticle { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private MessageConsumer<JsonObject> consumer; + + @Override + public void start(Promise<Void> startPromise) throws Exception { + consumer = new ServiceBinder(vertx) + .setAddress(IWhoisService.ADDRESS) + .register(IWhoisService.class, IWhoisService.create(vertx)); + consumer.completionHandler(ar -> { + if(ar.succeeded()) { + startPromise.complete(); + } else { + startPromise.fail(ar.cause()); + } + }); + } + + @Override + public void stop(Promise<Void> stopPromise) throws Exception { + consumer.unregister(stopPromise); + } +} + diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java new file mode 100644 index 0000000..8342b97 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java @@ -0,0 +1,4 @@ +@ModuleGen(groupPackage = "moe.yuuta.dn42peering.whois", name = "whois") +package moe.yuuta.dn42peering.whois; + +import io.vertx.codegen.annotations.ModuleGen;
\ No newline at end of file diff --git a/central/src/main/resources/asn/index.ftlh b/central/src/main/resources/asn/index.ftlh new file mode 100644 index 0000000..073866b --- /dev/null +++ b/central/src/main/resources/asn/index.ftlh @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <title>Register your ASN</title> +</head> +<body> +<h1>Register your ASN</h1> +<p>You may register your ASN now. One ASN can have multiple peers with us.</p> +<#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="/asn" method="post"> + <label for="asn">ASN (You cannot change this after registration):</label><br /> + <input type="text" id="asn" name="asn" required + placeholder="AS4242422980" + pattern="[aA][sS]424242[0-9][0-9][0-9][0-9]" + value="${input_asn}"><br /> + <br /> + + <label for="submit">When you click the following button, we will send an email with initial password to your tech-c person (All possible emails). Make sure you have access to that!</label><br /> + <label for="submit">If you did not receive the email, you may contact us for manual password reset.</label><br /> + <input type="submit" id="submit"> +</form> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/asn/success.ftlh b/central/src/main/resources/asn/success.ftlh new file mode 100644 index 0000000..15e6600 --- /dev/null +++ b/central/src/main/resources/asn/success.ftlh @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Register your ASN</title> +</head> +<body> +<h1>Your ASN is registered</h1> +<p>Please check your email for initial password. We had sent the password to the following emails addresses:</p> +<div> + <ul> + <#list emails as email> + <li>${email}</li> + </#list> + </ul> +</div> +<p>Important: You may re-register at any time if you did not rececive the email. However, upon one successful login, you will not be able to re-register.</p> +<a href="/manage">Login Now</a> | <a href="/">Back to homepage</a> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/index.ftlh b/central/src/main/resources/index.ftlh new file mode 100644 index 0000000..7b5051e --- /dev/null +++ b/central/src/main/resources/index.ftlh @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang=en> +<head> + <meta charset=utf-8> + <title>${name} dn42 Automatic Peering</title> +</head> +<body> +<h1>${name} dn42 Peering</h1> +<p><b>Already a peer? <a href="/manage">Login</a>.</b></p> +<h2>Peering Process</h2> +<ol> + <li>You: Register and verify your ASN.</li> + <li>You: Add a peer and submit your info.</li> + <li>We: Validate your info automatically.</li> + <li>We: If nothing goes wrong, we will automatically connect your endpoint.</li> + <li>You: Update your configuration to peer with us.</li> + <li>We: Constantly check your connection.</li> + <li>If you decided to stop peering with us, just login and delete your connection. We will stop connecting with you. The whole process is automated.</li> +</ol> +<h2>Special Notices (Please read)</h2> +<ul> + <li>Accounts are not shared across our endpoints.</li> +</ul> +<h2>Start peering today!</h2> +<ol> + <li>Register your ASN <a href="/asn">Here</a>.</li> + <li>Login to your <a href="/manage">Management Portal</a>.</li> + <li>Start peering by <a href="/manage/new">Creating a Peer</a>.</li> +</ol> +</body> +</html> diff --git a/central/src/main/resources/manage/changepw.ftlh b/central/src/main/resources/manage/changepw.ftlh new file mode 100644 index 0000000..481c0d0 --- /dev/null +++ b/central/src/main/resources/manage/changepw.ftlh @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Change Password | Manage your peering</title> +</head> +<body> +<h1>Change Password</h1> +<p>You are logged in as: ${asn}.</p> +<#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="/manage/change-password" method="post"> + <label for="passwd">New Password:</label><br /> + <input type="password" id="passwd" name="passwd" required + placeholder="p@ssw0rd!"><br /> + <br /> + <label for="confirm">Confirm Password:</label><br /> + <input type="password" id="confirm" name="confirm" required + placeholder="p@ssw0rd!"><br /> + <br /> + <input type="submit" id="submit"> +</form> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/manage/delete.ftlh b/central/src/main/resources/manage/delete.ftlh new file mode 100644 index 0000000..13453ae --- /dev/null +++ b/central/src/main/resources/manage/delete.ftlh @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Delete Account | Manage your peering</title> +</head> +<body> +<h1>Delete Account</h1> +<p>You are logged in as: ${asn}.</p> +<#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="/manage/delete-account" method="post"> + <label for="submit">Are you sure to delete your account? This will delete the ASN record on this node.</label><br /> + <input type="submit" id="submit"> +</form> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/manage/edit.ftlh b/central/src/main/resources/manage/edit.ftlh new file mode 100644 index 0000000..251566d --- /dev/null +++ b/central/src/main/resources/manage/edit.ftlh @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Edit Peer | Manage your peering</title> +</head> +<body> +<h1>Edit Peer</h1> +<p>You are logged in as: ${asn}.</p> +<#include "form.ftlh"> +<h2>More Actions</h2> +<a href="/manage/delete?id=${id}">Delete</a> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/manage/form.ftlh b/central/src/main/resources/manage/form.ftlh new file mode 100644 index 0000000..4beb3bb --- /dev/null +++ b/central/src/main/resources/manage/form.ftlh @@ -0,0 +1,60 @@ +<#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 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">dn42 IPv6 Address (Optional):</label><br /> + <input type="text" id="ipv6" name="ipv6" + placeholder="fe80::2980" + value="${(ipv6)!}"><br /> + <br /> + <input type="checkbox" id="mpbgp" name="mpbgp" value="mpbgp" ${mpbgp?string('checked', '')}> + <label for="mpbgp">Enable MP-BGP (Only works when you provide a IPv6 address)</label><br /><br /> + + <label>Node:</label><br /> + <#list nodes as node> + <input type="radio" id="node_${node.id}" name="node" value="${node.id}" required ${(node_checked == node.id)?string('checked', '')}> + <label for="node_${node.id}">${node.name} (${node.public_ip}, ${node.asn}, ${node.vpn_types?join(",")}) + <#if node.notice??><br />${node.notice?no_esc}</#if> + </label><br /> + </#list> + <br /> + + <label>Tunneling Method:</label><br /> + <input type="radio" id="wg" name="vpn" value="wg" required ${typeWireguard?string('checked', '')}> + <label for="wg">WireGuard</label><br /> + <br /> + + <label for="wg_endpoint"><b>WireGuard Specific Settings:</b></label><br /> + <label for="wg_endpoint">WireGuard Endpoint IP (Optional if you do not have a public IPv4 address):</label><br /> + <input type="text" id="wg_endpoint" name="wg_endpoint" + placeholder="114.51.4.191" + pattern="[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" + value="${(wgEndpoint)!}"><br /> + <br /> + <label for="wg_endpoint_port">WireGuard Endpoint Port (Optional if you do not have a public IPv4 address):</label><br /> + <input type="text" id="wg_endpoint_port" name="wg_endpoint_port" + placeholder="19198" + pattern="[0-9]+" + value="${(wgEndpointPort?long?c)!}"><br /> + <br /> + <label for="wg_pubkey">WireGuard Public Key:</label><br /> + <input type="text" id="wg_pubkey" name="wg_pubkey" required + placeholder="CdX7EQwezTPaVjhp1Kw29RsmYYN3nTjPxOln4WaPOAU=" + value="${(wgPubkey)!}"><br /> + <br /> + + <input type="submit"> +</form>
\ No newline at end of file diff --git a/central/src/main/resources/manage/index.ftlh b/central/src/main/resources/manage/index.ftlh new file mode 100644 index 0000000..983dc63 --- /dev/null +++ b/central/src/main/resources/manage/index.ftlh @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Manage your peering</title> +</head> +<body> +<h1>Manage your peering</h1> +<p>You are logged in as: ${asn}.</p> +<h2>Peers</h2> +<p><a href="/manage/new">New Peer</a></p> +<table style="width: 100%"> + <tr> + <th>IPv4</th> + <th>IPv6</th> + <th>Method</th> + <th>Provision</th> + <th>Actions</th> + </tr> + <#list peers as peer> + <tr> + <td>${peer.ipv4}</td> + <td><#if peer.ipv6??>${peer.ipv6}<#else>None</#if></td> + <td>${peer.type}</td> + <td>${peer.provisionStatus}</td> + <td><a href="/manage/edit?id=${peer.id}">Edit</a> | + <a href="/manage/show-configuration?id=${peer.id}">Example Conf</a></td> + </tr> + </#list> +</table> +<h2>More Actions</h2> +<a href="/manage/change-password">Change Password</a> | +<a href="/manage/delete-account">Delete My Account</a> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/manage/new.ftlh b/central/src/main/resources/manage/new.ftlh new file mode 100644 index 0000000..9b572ec --- /dev/null +++ b/central/src/main/resources/manage/new.ftlh @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Add New Peer | Manage your peering</title> +</head> +<body> +<h1>Add New Peer</h1> +<p>You are logged in as: ${asn}.</p> +<#include "form.ftlh"> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/manage/showconf.ftlh b/central/src/main/resources/manage/showconf.ftlh new file mode 100644 index 0000000..5c5f24d --- /dev/null +++ b/central/src/main/resources/manage/showconf.ftlh @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Peering Configuration</title> +</head> +<body> +<h1>Peering Configuration</h1> +<p>Here are the information needed for you to setup:</p> +<ul> + <li>WireGuard Endpoint: ${endpoint}:${wgPort}</li> + <li>dn42 IPv4 Address: ${ipv4}</li> + <li>dn42 IPv6 Address: ${ipv6}</li> + <li>ASN: ${asn}</li> + <li>WireGuard Public Key: ${wgSelfPubkey}</li> + <li>WireGuard Preshared Secret: ${wgPresharedSecret}</li> + <li>MP-BGP: ${mpbgp?string('Yes', 'No')}</li> +</ul> +<a href="/manage">Back</a> +</body> +</html>
\ No newline at end of file diff --git a/docs/QuickStart.md b/docs/QuickStart.md new file mode 100644 index 0000000..ed42ef5 --- /dev/null +++ b/docs/QuickStart.md @@ -0,0 +1,53 @@ +# Quick Start + +This is a quick guide of setting dn42peering up. + +## Prepare a local copy of dn42 registry and regularly update it + +You need to have a local copy of the dn42 registry, and regularly update it. + +Important: You need to delete all contents from `data/inetnum` and `data/inet6num`. + +Finally, enable and start `whois42d` service. + +## Prepare the SQL database + +Create a database in a MySQL instance, then create required tables. + +Refer to [Database](central/Database.md) for more details. + +## Install the central + +Go to the download section and download the prebuilt central package, or build it yourself. + +## Create a VPN tunnel between central and your first node + +You may want to use WireGuard or similar VPN technologies to establish a dedicated tunnel between them. + +Please notice that the communication between central and agent is not encrypted, so be sure to use a VPN. + +Also, do not conflict the VPN address with dn42 addresses. + +## Install the agent + +Go to the download section and download the prebuilt agent package, or build it yourself. + +## Register the agent in the database and configure the agent + +Create a record in the database / node table as [agent.sql](sql/agent.sql) + +Create the agent configuration as shown in [Agent Configuration](agent/Configuration.md) + +Then, start the agent. + +## Register a mail service account and get the SMTP credentials + +For example, MailGun. + +## Configure the central + +See [Central Configuration](central/Configuration.md) + +## Start the central + +Finally, start the central.
\ No newline at end of file diff --git a/docs/agent/Configuration.md b/docs/agent/Configuration.md new file mode 100644 index 0000000..9d9af8f --- /dev/null +++ b/docs/agent/Configuration.md @@ -0,0 +1,21 @@ +# Configuration references + +The configuration format of agent is JSON. + +## Reference + +```json +{ + "internal_ip": "<Your internal controlling IPv4 address without port, see below # Controlling>" +} +``` + +For both IP addresses, prefixes must not be included. + +## Controlling + +The central communicates with agents using gRPC. An internal IP address is required for that. + +The only requirement for internal IP address is that the agent can communicate with central. Agents are not required to communicate with each other. + +Though is it OK to use the dn42 IP address, it is strongly recommended to create a separete VPN tunnel between each agent and central combination, as some provision failure could cause the agent to disconnected.
\ No newline at end of file diff --git a/docs/central/Configuration.md b/docs/central/Configuration.md new file mode 100644 index 0000000..05cf3bb --- /dev/null +++ b/docs/central/Configuration.md @@ -0,0 +1,46 @@ +# Configuration references + +The configuration format of central is JSON. + +## Reference + +```json +{ + "database": { + "port": 3306, + "host": "host.name.or.ip.for.MySQL.database", + "database": "mysql database", + "user": "test", + "password": "123456" + }, + "http": { + "name": "<Site name> It will appear like <name> dn42 peering." + }, + "mail": { + "hostname": "SMTP hostname", + "port": 587, + "starttls": "DISABLED / OPTIONAL / REQUIRED", + "username": "SMTP username", + "password": "SMTP password", + "from": "Postmaster <postmaster@example.tld>" + }, + "whois": "localhost (Whois hostname. See below # Whois)" +} +``` + +## Database + +A MySQL database with predefined schema is required for central to operate. See [Database](Database.md) section for more details. + +## SMTP + +A smtp server is required for email verification. A recommendation is mail services like MailGun. + +## Whois + +A whois server that is capable of looking up dn42 ASN's and routes is required for ASN and IP verification. + +An example is [whois42d](https://github.com/Mic92/whois42d), but make sure you delete all contents under `data/inetnum` and `data/inet6num` or whois42d will not lookup routes. + +## Configuration Location and Reloading + diff --git a/docs/central/Database.md b/docs/central/Database.md new file mode 100644 index 0000000..bfe014b --- /dev/null +++ b/docs/central/Database.md @@ -0,0 +1,9 @@ +# Database + +A MySQL database is required for dn42peering central to work. Before installing the central program, make sure you have a proper database with tables setup. + +Here is the SQL used to setup a production database: + +See [0-init.sql](sql/0-init.sql) for creating initial schema. + +Note that the SQL script will update with schema updates. Please follow the docs/sql/ with the latest version.
\ No newline at end of file diff --git a/docs/central/sql/0-init.sql b/docs/central/sql/0-init.sql new file mode 100644 index 0000000..17ca074 --- /dev/null +++ b/docs/central/sql/0-init.sql @@ -0,0 +1,57 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; + +CREATE TABLE `asn` ( + `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `password_hash` text COLLATE utf8mb4_unicode_ci NOT NULL, + `activated` bit(1) NOT NULL DEFAULT b'0', + `register_date` timestamp NOT NULL DEFAULT current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE `node` ( + `id` int(10) UNSIGNED NOT NULL, + `public_ip` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL, + `dn42_ip4` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL, + `dn42_ip6` varchar(39) COLLATE utf8mb4_unicode_ci NOT NULL, + `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `internal_ip` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL, + `internal_port` smallint(5) UNSIGNED NOT NULL, + `name` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `notice` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `vpn_type_wg` tinyint(1) NOT NULL DEFAULT 1 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE `peer` ( + `id` int(11) NOT NULL, + `type` enum('WIREGUARD') COLLATE utf8mb4_unicode_ci NOT NULL, + `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `ipv4` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL, + `ipv6` varchar(39) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `wg_endpoint` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `wg_endpoint_port` smallint(5) UNSIGNED DEFAULT NULL, + `wg_self_pubkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `wg_self_privkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `wg_peer_pubkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `wg_preshared_secret` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `provision_status` enum('NOT_PROVISIONED','PROVISIONED','FAIL') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NOT_PROVISIONED', + `mpbgp` tinyint(1) NOT NULL, + `node` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +ALTER TABLE `asn` + ADD PRIMARY KEY (`asn`); + +ALTER TABLE `node` + ADD PRIMARY KEY (`id`); + +ALTER TABLE `peer` + ADD PRIMARY KEY (`id`); + + +ALTER TABLE `node` + MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT; + +ALTER TABLE `peer` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +COMMIT; diff --git a/docs/central/sql/agent.sql b/docs/central/sql/agent.sql new file mode 100644 index 0000000..2ec84bf --- /dev/null +++ b/docs/central/sql/agent.sql @@ -0,0 +1,24 @@ +-- Use this script to add a new node to the database. +-- Note: Current node support is minimal: Do not edit or delete the node after creation. +-- A GUI will be provided later with full support. + +INSERT INTO `node` +(public_ip, +dn42_ip4, +dn42_ip6, +asn, +internal_ip, +internal_port, +name, +notice, +vpn_type_wg) +VALUES +('127.0.0.1', -- The public IP address to display +'172.23.105.1', -- The dn42 IPv4 address (No prefixes) +'fe80:2980::1', -- The dn42 or link local IPv6 address (No prefixes) +'AS4242422980', -- The ASN of this node to display. It is possible to have multiple ASNs in different nodes. +'127.0.0.1', -- The internal address for management. See agent/Configuration.md for more details. Must be the same with agent configuration. +49200, -- Currently only support 49200 +'North America GCP', -- Display name +'<b>North America</b> 的 GCP', -- Optional notice to display. Support HTML. +1)
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..e708b1c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..be52383 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/rpc-common/build.gradle b/rpc-common/build.gradle new file mode 100644 index 0000000..81bf422 --- /dev/null +++ b/rpc-common/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' + id "com.google.protobuf" version "0.8.14" + id 'idea' +} + +group 'moe.yuuta' +version '1.0' + +def protocVersion = protobufVersion + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' + implementation "io.vertx:vertx-core:${project.vertxVersion}" + implementation "io.vertx:vertx-grpc:${project.vertxVersion}" + implementation "io.grpc:grpc-protobuf:${project.grpcVersion}" + implementation "io.grpc:grpc-stub:${project.grpcVersion}" +} + +test { + useJUnitPlatform() +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protocVersion" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${project.grpcVersion}" + } + vertx { + artifact = "io.vertx:vertx-grpc-protoc-plugin:${project.vertxVersion}" + } + } + generateProtoTasks { + all()*.plugins { + grpc + vertx + } + } +}
\ No newline at end of file diff --git a/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java new file mode 100644 index 0000000..3fe243b --- /dev/null +++ b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java @@ -0,0 +1,5 @@ +package moe.yuuta.dn42peering; + +public final class RPC { + public static final int AGENT_PORT = 49200; +} diff --git a/rpc-common/src/main/proto/agent.proto b/rpc-common/src/main/proto/agent.proto new file mode 100644 index 0000000..126fbd2 --- /dev/null +++ b/rpc-common/src/main/proto/agent.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "moe.yuuta.dn42peering.agent.proto"; +option java_outer_classname = "AgentProto"; +package moe.yuuta.dn42peering.agent; + +service Agent { + rpc ProvisionBGP (BGPRequest) returns (BGPReply) {} + rpc ReloadBGP (BGPRequest) returns (BGPReply) {} + rpc DeleteBGP (BGPRequest) returns (BGPReply) {} + + rpc ProvisionWG (WGRequest) returns (WGReply) {} + rpc ReloadWG (WGRequest) returns (WGReply) {} + rpc DeleteWG (WGRequest) returns (WGReply) {} +} + +message BGPRequest { + Node node = 1; + uint64 id = 2; + string asn = 3; + bool mpbgp = 4; + string ipv4 = 5; + string ipv6 = 6; + string device = 7; +} + +message BGPReply { +} + +message WGRequest { + Node node = 1; + uint64 id = 2; + uint32 listenPort = 3; + string endpoint = 4; + string peerPubKey = 5; + string selfPrivKey = 6; + string selfPresharedSecret = 7; + string peerIPv4 = 8; + string peerIPv6 = 9; +} + +message WGReply { + string device = 1; +} + +message Node { + uint64 id = 1; + string ipv4 = 2; + string ipv6 = 3; +}
\ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e75b493 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'dn42peering' +include ':central', ':agent', ':rpc-common'
\ No newline at end of file |