diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..86466d9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build +on: + push: + branches: + - main + pull_request: + +permissions: + actions: write + checks: write + contents: read + deployments: none + issues: none + discussions: none + packages: none + pages: none + pull-requests: write + security-events: write + statuses: write + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + java: [21] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + - name: Build with Maven + run: mvn --batch-mode clean install diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4d1e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Build tools +.gradle/ +.gradle +.project +gradle.properties +target/ +/build + +# Compiled class file +*.class + +# Log file +*.log + + +# IntelliJ files +.idea/ +.classpath +*.iml +out/ + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# macOS nonsense +.DS_Store \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..23c7e59 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0680bd2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Start Debugger", + "request": "attach", + "hostName": "localhost", + "port": 5005 + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7f9e41c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive", + "java.import.gradle.enabled": false +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..da751c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vonage Community + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e66e223 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Vonage Hackathon Remote Code Execution Demo + +[![Build Status](https://github.com/Vonage-Community/sample-serversdk-java-springboot/actions/workflows/build.yml/badge.svg)](https://github.com/SMadani/vonage-hackathon-rce/actions/workflows/build.yml") +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +This repo demonstrates how you can use the Messages API to execute shell commands on your server. +It is built using the [Vonage Java Server SDK](https://github.com/Vonage/vonage-java-sdk) and Spring Boot 3. + +## Pre-requisites +You will need Java Development Kit 21 or later to run this demo. +Installation instructions can be found [here for Temurin JDKs](https://adoptium.net/en-GB/installation/) or +[here for Oracle JDK 21](https://docs.oracle.com/en/java/javase/21/install/overview-jdk-installation.html). + +## Configuration +All the parameters required to run the demo can be provided through environment variables. These are as follows: + +- `VONAGE_API_KEY`: Vonage account API key. +- `VONAGE_API_SECRET`: Vonage account API secret. +- `VONAGE_APPLICATION_ID`: Vonage application UUID. +- `VONAGE_PRIVATE_KEY_PATH`: Absolute path to the private key associated with your Vonage application. +- `VCR_PORT`: Port to run the demo on. By default, this is `8080`. + +## Build & Run +If you have [IntelliJ IDEA](https://www.jetbrains.com/idea/) installed, you can import this project +and run it through the IDE, where the entry point is the `Application` class +(src/main/java/com/vonage/hackathon/rce/Application.java). + +To run the demo standalone from the command line, do `mvn install spring-boot:run`. +Then open a browser to [localhost:8080](http://localhost:8080) to use the application. + + diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8072a08 --- /dev/null +++ b/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + com.vonage + hackathon-messages-rce + 0.0.1-SNAPSHOT + + RCE via Vonage Java SDK + Hackathon project showcasing Remote Code Execution over Messages API. + https://github.com/SMadani/vonage-hackathon-rce + + + Vonage Community + https://developer.vonage.com + + + + + MIT + ${project.url}/blob/main/LICENCE.txt + + + + + + SMadani + Sina Madani + sina.madani@vonage.com + + + + + scm:git@github.com:SMadani/vonage-hackathon-rce + ${project.url} + + + GitHub + ${project.url}/issues + + + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + + com.vonage.hackathon.rce.Application + 21 + + + + + com.vonage + server-sdk + 8.10.0 + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + + debug + + + -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 + + + + + + + maven-assembly-plugin + + + + ${exec.mainClass} + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/vonage/hackathon/rce/Application.java b/src/main/java/com/vonage/hackathon/rce/Application.java new file mode 100644 index 0000000..f0003d1 --- /dev/null +++ b/src/main/java/com/vonage/hackathon/rce/Application.java @@ -0,0 +1,13 @@ +package com.vonage.hackathon.rce; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +@SpringBootApplication +@ConfigurationPropertiesScan +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/vonage/hackathon/rce/ApplicationConfiguration.java b/src/main/java/com/vonage/hackathon/rce/ApplicationConfiguration.java new file mode 100644 index 0000000..7777e56 --- /dev/null +++ b/src/main/java/com/vonage/hackathon/rce/ApplicationConfiguration.java @@ -0,0 +1,109 @@ +package com.vonage.hackathon.rce; + +import com.vonage.client.VonageClient; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.UUID; + +@ConfigurationProperties(prefix = "vonage") +public class ApplicationConfiguration { + //private static final URI EXTERNAL_IP_SERVICE_URL = URI.create("http://checkip.amazonaws.com/"); + + //final HttpClient javaHttpClient = HttpClient.newHttpClient(); + final VonageClient vonageClient; + + @Bean + public WebServerFactoryCustomizer webServerFactoryCustomizer() { + return factory -> { + getEnv("VCR_PORT").map(Integer::parseInt).ifPresent(factory::setPort); + try { + factory.setAddress(InetAddress.getByAddress(new byte[]{0,0,0,0})); + } + catch (UnknownHostException ex) { + throw new IllegalStateException(ex); + } + }; + } + + record VonageCredentials(String apiKey, String apiSecret, String applicationId, String privateKey) {} + + private static Optional getEnv(String env) { + return Optional.ofNullable(System.getenv(env)); + } + + private static String getEnvWithAlt(String primary, String fallbackEnv) { + return getEnv(primary).orElseGet(() -> System.getenv(fallbackEnv)); + } + + /*private String getExternalIp() { + var request = HttpRequest.newBuilder().uri(EXTERNAL_IP_SERVICE_URL).GET().build(); + try { + return javaHttpClient.send(request, HttpResponse.BodyHandlers.ofString()).body(); + } + catch (IOException | InterruptedException ex) { + throw new IllegalStateException("Could not get external IP", ex); + } + }*/ + + @ConstructorBinding + ApplicationConfiguration(VonageCredentials credentials) { + var clientBuilder = VonageClient.builder(); + var apiKey = getEnvWithAlt("VONAGE_API_KEY", "VCR_API_ACCOUNT_ID"); + var apiSecret = getEnvWithAlt("VONAGE_API_SECRET", "VCR_API_ACCOUNT_SECRET"); + var applicationId = getEnvWithAlt("VONAGE_APPLICATION_ID", "VCR_API_APPLICATION_ID"); + var privateKey = getEnvWithAlt("VONAGE_PRIVATE_KEY_PATH", "VCR_PRIVATE_KEY"); + + if (credentials != null) { + if (credentials.apiKey != null && !credentials.apiKey.isEmpty()) { + apiKey = credentials.apiKey; + } + if (credentials.apiSecret != null && !credentials.apiSecret.isEmpty()) { + apiSecret = credentials.apiSecret; + } + if (credentials.applicationId != null && !credentials.applicationId.isEmpty()) { + applicationId = credentials.applicationId; + } + if (credentials.privateKey != null && !credentials.privateKey.isEmpty()) { + privateKey = credentials.privateKey; + } + } + + if (privateKey != null && applicationId != null) { + try { + var uuid = UUID.fromString(applicationId); + assert uuid.version() > 0; + if (privateKey.startsWith("-----BEGIN PRIVATE KEY-----")) { + clientBuilder.privateKeyContents(privateKey.getBytes()); + } + else { + clientBuilder.privateKeyPath(Paths.get(privateKey)); + } + clientBuilder.applicationId(applicationId); + } + catch (InvalidPathException ipx) { + System.err.println("Invalid path or private key: "+privateKey); + } + catch (IllegalArgumentException iax) { + System.err.println("Invalid application ID: "+applicationId); + } + } + if (apiKey != null && apiKey.length() >= 7 && apiSecret != null && apiSecret.length() >= 16) { + clientBuilder.apiKey(apiKey).apiSecret(apiSecret); + } + + vonageClient = clientBuilder.build(); + } +} diff --git a/src/main/java/com/vonage/hackathon/rce/ApplicationController.java b/src/main/java/com/vonage/hackathon/rce/ApplicationController.java new file mode 100644 index 0000000..6668365 --- /dev/null +++ b/src/main/java/com/vonage/hackathon/rce/ApplicationController.java @@ -0,0 +1,93 @@ +package com.vonage.hackathon.rce; + +import com.vonage.client.messages.InboundMessage; +import com.vonage.client.messages.MessageStatus; +import com.vonage.client.messages.sms.SmsTextRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.logging.Logger; + +@Controller +public final class ApplicationController { + private final Logger logger = Logger.getLogger("controller"); + + String verifiedNumber = System.getenv("TO_NUMBER"); + + @Autowired + private ApplicationConfiguration configuration; + + private String standardWebhookResponse() { + return "OK"; + } + + private void sendMessage(String from, String text) { + int threshold = 950, length = text.length(); + if (length > threshold) { + logger.info("Long message ("+length+" characters). Sending in parts..."); + } + var parts = new String[length / threshold + 1]; + for (int i = 0; i < parts.length; i++) { + parts[i] = text.substring(i * threshold, Math.min(length, (i + 1) * threshold)); + } + + var builder = SmsTextRequest.builder().from(from).to(verifiedNumber); + + for (var part : parts) { + logger.info("Message sent: " + configuration.vonageClient.getMessagesClient() + .sendMessage(builder.text(part).build()).getMessageUuid() + ); + } + } + + @ResponseBody + @GetMapping("/_/health") + public String health() { + return standardWebhookResponse(); + } + + @ResponseBody + @PostMapping("/webhooks/messages/status") + public String messageStatus(@RequestBody MessageStatus status) { + logger.info("Received message status: "+status.toJson()); + return standardWebhookResponse(); + } + + @SuppressWarnings("StatementWithEmptyBody") + @ResponseBody + @PostMapping("/webhooks/messages/inbound") + public String inboundMessage(@RequestBody InboundMessage inbound) throws IOException { + logger.info("Received inbound message: "+inbound.toJson()); + var command = inbound.getText(); + if (command != null && !command.isBlank()) { + logger.info("Executing command: "+command); + var process = new ProcessBuilder().redirectErrorStream(true) + .command("sh", "-cr", command).start(); + + String parsedOutput; + + try (var stream = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + var output = new StringBuilder(); + for (String line; (line = stream.readLine()) != null; output.append(line).append("\n")); + process.waitFor(); + parsedOutput = output.toString().trim(); + logger.info("Process output: " + parsedOutput); + } + catch (InterruptedException ex) { + parsedOutput = "Process interrupted: "+ex.getMessage(); + logger.warning(parsedOutput); + } + + sendMessage(inbound.getTo(), parsedOutput.isBlank() ? + "Exit value "+process.exitValue() : parsedOutput + ); + } + else { + logger.warning("No command received."); + } + return standardWebhookResponse(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..715ca99 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +vonage.credentials.applicationId= +vonage.credentials.privateKey= +vonage.credentials.apiKey= +vonage.credentials.apiSecret=