diff --git a/.github/workflows/test-submodules.yml b/.github/workflows/test-submodules.yml new file mode 100644 index 00000000..f2f16742 --- /dev/null +++ b/.github/workflows/test-submodules.yml @@ -0,0 +1,222 @@ +name: Test Submodules for Breaking Changes + +on: + workflow_dispatch: + inputs: + submodule_name: + description: 'Submodule to test (leave empty to test all)' + required: false + type: choice + options: + - 'all' + - 'ruby-v2-server/local-ruby-sdk' + - 'ruby-v3-server/local-ruby-sdk' + - 'php-v2-server/local-php-sdk' + - 'php-v3-server/local-php-sdk' + - 'java-v3-transition-server/s3ec-staging' + - 'java-v4-server/s3ec-staging' + default: 'all' + ruby_v2_branch: + description: 'Branch for ruby-v2-server/local-ruby-sdk (default: main)' + required: false + type: string + default: '' + ruby_v3_branch: + description: 'Branch for ruby-v3-server/local-ruby-sdk (default: main)' + required: false + type: string + default: '' + php_v2_branch: + description: 'Branch for php-v2-server/local-php-sdk (default: s3ec/transitional)' + required: false + type: string + default: '' + php_v3_branch: + description: 'Branch for php-v3-server/local-php-sdk (default: s3ec/improved)' + required: false + type: string + default: '' + java_v3_transition_branch: + description: 'Branch for java-v3-transition-server/s3ec-staging (default: s3ec/transitional)' + required: false + type: string + default: '' + java_v4_branch: + description: 'Branch for java-v4-server/s3ec-staging (default: s3ec/improved)' + required: false + type: string + default: '' + +jobs: + test-submodules: + runs-on: macos-13 + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_FOR_PRIVATE_RUBY }} + + - name: Update submodules to target branches + run: | + echo "## Submodule Update Summary" >> $GITHUB_STEP_SUMMARY + echo "Updating submodules to target branches..." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Determine target branches for each submodule (use input or default from .gitmodules) + ruby_v2_target="${{ inputs.ruby_v2_branch }}" + if [ -z "$ruby_v2_target" ]; then + ruby_v2_target="main" # Default branch for ruby submodules (no branch specified in .gitmodules) + fi + + ruby_v3_target="${{ inputs.ruby_v3_branch }}" + if [ -z "$ruby_v3_target" ]; then + ruby_v3_target="main" # Default branch for ruby submodules (no branch specified in .gitmodules) + fi + + php_v2_target="${{ inputs.php_v2_branch }}" + if [ -z "$php_v2_target" ]; then + php_v2_target="s3ec/transitional" # Default from .gitmodules + fi + + php_v3_target="${{ inputs.php_v3_branch }}" + if [ -z "$php_v3_target" ]; then + php_v3_target="s3ec/improved" # Default from .gitmodules + fi + + java_v3_transition_target="${{ inputs.java_v3_transition_branch }}" + if [ -z "$java_v3_transition_target" ]; then + java_v3_transition_target="s3ec/transitional" # Default from .gitmodules + fi + + java_v4_target="${{ inputs.java_v4_branch }}" + if [ -z "$java_v4_target" ]; then + java_v4_target="s3ec/improved" # Default from .gitmodules + fi + + # Function to update a submodule if it should be tested + update_submodule() { + local submodule_name="$1" + local submodule_path="$2" + local target_branch="$3" + + if [ "${{ inputs.submodule_name }}" = "all" ] || [ "${{ inputs.submodule_name }}" = "$submodule_name" ]; then + echo "Updating $submodule_name to branch: $target_branch" + echo "- **$submodule_name**: $target_branch" >> $GITHUB_STEP_SUMMARY + + cd "$submodule_path" + current_commit=$(git rev-parse HEAD) + echo " Current commit: $current_commit" + + git fetch origin "$target_branch" + git checkout "$target_branch" + git pull origin "$target_branch" + new_commit=$(git rev-parse HEAD) + echo " Updated to commit: $new_commit" + + cd - > /dev/null + fi + } + + # Update submodules based on selection + update_submodule "ruby-v2-server/local-ruby-sdk" "test-server/ruby-v2-server/local-ruby-sdk" "$ruby_v2_target" + update_submodule "ruby-v3-server/local-ruby-sdk" "test-server/ruby-v3-server/local-ruby-sdk" "$ruby_v3_target" + update_submodule "php-v2-server/local-php-sdk" "test-server/php-v2-server/local-php-sdk" "$php_v2_target" + update_submodule "php-v3-server/local-php-sdk" "test-server/php-v3-server/local-php-sdk" "$php_v3_target" + update_submodule "java-v3-transition-server/s3ec-staging" "test-server/java-v3-transition-server/s3ec-staging" "$java_v3_transition_target" + update_submodule "java-v4-server/s3ec-staging" "test-server/java-v4-server/s3ec-staging" "$java_v4_target" + + echo "" >> $GITHUB_STEP_SUMMARY + echo "All specified submodules have been updated." >> $GITHUB_STEP_SUMMARY + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + + - name: Set up PHP with Composer + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + + - name: Install PHP V2 dependencies + working-directory: ./test-server/php-v2-server + shell: bash + run: composer install + + - name: Install PHP V3 dependencies + working-directory: ./test-server/php-v3-server + shell: bash + run: composer install + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.25 + + # Cache uv dependencies + - name: Cache uv dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Install Uv + run: pip install uv + + # Cache Gradle dependencies and build outputs + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + test-server/java-v3-server/.gradle + test-server/java-tests/.gradle + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Install dependencies + run: make install + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::370957321024:role/S3EC-Python-Github-test-role + aws-region: us-west-2 + + - name: Run unit tests + run: make test-unit + + - name: Run integration tests + run: make test-integration + env: + CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }} + CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }} + + - name: Run test-server tests + run: cd test-server && make ci + env: + AWS_REGION: us-west-2 + TEST_SERVER_S3_BUCKET: ${{ vars.TEST_SERVER_S3_BUCKET }} + TEST_SERVER_KMS_KEY_ARN: ${{ vars.TEST_SERVER_KMS_KEY_ARN }} + GRADLE_OPTS: "-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" + + - name: Upload results + if: always() + uses: actions/upload-artifact@v4 + with: + name: submodule-test-results + path: test-server/java-tests/build/reports/tests/integ diff --git a/.gitmodules b/.gitmodules index ce2abc73..213d0741 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,11 @@ path = test-server/php-v3-server/local-php-sdk url = git@github.com:aws/private-aws-sdk-php-staging.git branch = s3ec/improved +[submodule "test-server/java-v3-transition-server/s3ec-staging"] + path = test-server/java-v3-transition-server/s3ec-staging + url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git + branch = s3ec/transitional +[submodule "test-server/java-v4-server/s3ec-staging"] + path = test-server/java-v4-server/s3ec-staging + url = git@github.com:aws/private-amazon-s3-encryption-client-java-staging.git + branch = s3ec/improved \ No newline at end of file diff --git a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java index 395d11c7..c5d6d393 100644 --- a/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java +++ b/test-server/java-tests/src/it/java/software/amazon/encryption/s3/TestUtils.java @@ -124,13 +124,13 @@ public class TestUtils { servers.put(RUBY_V3, new LanguageServerTarget(RUBY_V3, "8092")); servers.put(PHP_V3, new LanguageServerTarget(PHP_V3, "8093")); // TODO: Create and add transition servers - // servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); + servers.put(JAVA_V3_TRANSITION, new LanguageServerTarget(JAVA_V3_TRANSITION, "8094")); // servers.put(GO_V3_TRANSITION, new LanguageServerTarget(GO_V3_TRANSITION, "8095")); // servers.put(NET_V2_TRANSITION, new LanguageServerTarget(NET_V2_TRANSITION, "8096")); // servers.put(CPP_V2_TRANSITION, new LanguageServerTarget(CPP_V2_TRANSITION, "8097")); // servers.put(RUBY_V2_TRANSITION, new LanguageServerTarget(RUBY_V2_TRANSITION, "8098")); // servers.put(PHP_V2_TRANSITION, new LanguageServerTarget(PHP_V2_TRANSITION, "8099")); - + servers.put(JAVA_V4, new LanguageServerTarget(JAVA_V4, "8090")); serverMap = filterServers(servers); } diff --git a/test-server/java-v3-transition-server/Makefile b/test-server/java-v3-transition-server/Makefile new file mode 100644 index 00000000..b4371d0a --- /dev/null +++ b/test-server/java-v3-transition-server/Makefile @@ -0,0 +1,30 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server build-s3ec + +PID_FILE := server.pid +PORT := 8094 + +build-s3ec: + @echo "Building S3EC from source..." + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile + cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "S3EC build completed." + +start-server: build-s3ec + @echo "Starting Java V3 server..." + AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \ + AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ + AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ + AWS_REGION="us-west-2" \ + ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + @echo "Java V3 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v3-transition-server/README.md b/test-server/java-v3-transition-server/README.md new file mode 100644 index 00000000..5f08cc1c --- /dev/null +++ b/test-server/java-v3-transition-server/README.md @@ -0,0 +1,23 @@ +# S3EC Java V3 Test Server + +This is the Java implementation of the S3ECTestServer framework for S3EC Java V3. It provides a server implementation for testing Java S3 Encryption Client V3 functionality. + +## Overview + +The S3ECJavaTestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +gradle run +``` + +This will start the server running on port `8094`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v3-transition-server/build.gradle.kts b/test-server/java-v3-transition-server/build.gradle.kts new file mode 100644 index 00000000..b874d8e2 --- /dev/null +++ b/test-server/java-v3-transition-server/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") + application +} + +dependencies { + val smithyJavaVersion: String by project + + smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion") + + implementation("software.amazon.smithy:smithy-rules-engine:1.59.0") + implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") + implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") + + // S3EC from local Maven repository (installed by mvn install) + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-SNAPSHOT") +} + +// Use that application plugin to start the service via the `run` task. +application { + mainClass = "software.amazon.encryption.s3.S3ECJavaTestServer" +} + +// Add generated Java files to the main sourceSet +afterEvaluate { + val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen") + sourceSets { + main { + java { + srcDir(serverPath) + } + } + } +} + +tasks { + compileJava { + dependsOn(smithyBuild) + } +} + +// Helps Intellij IDE's discover smithy models +sourceSets { + main { + java { + srcDir("../model") + } + } +} + +repositories { + mavenLocal() + mavenCentral() +} diff --git a/test-server/java-v3-transition-server/gradle.properties b/test-server/java-v3-transition-server/gradle.properties new file mode 100644 index 00000000..08afce82 --- /dev/null +++ b/test-server/java-v3-transition-server/gradle.properties @@ -0,0 +1,11 @@ +# Smithy versions +smithyJavaVersion=[0,1] +smithyGradleVersion=1.1.0 +smithyVersion=[1,2] + +# Performance optimization settings +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.workers.max=4 diff --git a/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e6441136 Binary files /dev/null and b/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.properties b/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a4413138 --- /dev/null +++ b/test-server/java-v3-transition-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/test-server/java-v3-transition-server/gradlew b/test-server/java-v3-transition-server/gradlew new file mode 100755 index 00000000..b740cf13 --- /dev/null +++ b/test-server/java-v3-transition-server/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/test-server/java-v3-transition-server/gradlew.bat b/test-server/java-v3-transition-server/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/test-server/java-v3-transition-server/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test-server/java-v3-transition-server/license.txt b/test-server/java-v3-transition-server/license.txt new file mode 100644 index 00000000..2dd564b3 --- /dev/null +++ b/test-server/java-v3-transition-server/license.txt @@ -0,0 +1,4 @@ +/* + * Example file license header. + * File header line two + */ \ No newline at end of file diff --git a/test-server/java-v3-transition-server/s3ec-staging b/test-server/java-v3-transition-server/s3ec-staging new file mode 160000 index 00000000..c572d958 --- /dev/null +++ b/test-server/java-v3-transition-server/s3ec-staging @@ -0,0 +1 @@ +Subproject commit c572d9587ee934aad6a2fe8091a72080846d32af diff --git a/test-server/java-v3-transition-server/settings.gradle.kts b/test-server/java-v3-transition-server/settings.gradle.kts new file mode 100644 index 00000000..e7c41714 --- /dev/null +++ b/test-server/java-v3-transition-server/settings.gradle.kts @@ -0,0 +1,19 @@ +/** + * Basic usage of generated server stubs. + */ + +pluginManagement { + val smithyGradleVersion: String by settings + + plugins { + id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + } + + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "S3ECJavaTestServer" diff --git a/test-server/java-v3-transition-server/smithy-build.json b/test-server/java-v3-transition-server/smithy-build.json new file mode 100644 index 00000000..a0fcb8e5 --- /dev/null +++ b/test-server/java-v3-transition-server/smithy-build.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "plugins": { + "java-server-codegen": { + "service": "software.amazon.encryption.s3#S3ECTestServer", + "namespace": "software.amazon.encryption.s3", + "headerFile": "license.txt" + } + }, + "sources": ["../model"] +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java new file mode 100644 index 00000000..d992c435 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -0,0 +1,109 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.traits.Trait; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.Keyring; +import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.service.CreateClientOperation; + +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CreateClientOperationImpl implements CreateClientOperation { + private Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } + } + + return haveOneNonNull; + } + + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { + try { + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw new RuntimeException(nse); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + S3Client s3Client = S3EncryptionClient.builder() + .keyring(keyring) + .build(); + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java new file mode 100644 index 00000000..e7c5493f --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -0,0 +1,72 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.GetObjectInput; +import software.amazon.encryption.s3.model.GetObjectOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.GetObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; +import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; + +public class GetObjectOperationImpl implements GetObjectOperation { + private Map clientCache_; + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + S3Client s3Client = clientCache_.get(input.getClientID()); + Map ecMap = metadataListToMap(input.getMetadata()); + + try { + ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(ecMap))); + + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } catch (S3EncryptionClientException s3EncryptionClientException) { + // Modeled exceptions MUST be returned as such + StringWriter sw = new StringWriter(); + s3EncryptionClientException.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw S3EncryptionClientError.builder() + .message(stackTrace) + .build(); + } + } catch (Exception e) { + // Don't wrap modeled errors + if (e instanceof S3EncryptionClientError) { + throw e; + } + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java new file mode 100644 index 00000000..036289ec --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -0,0 +1,43 @@ +package software.amazon.encryption.s3; + +import software.amazon.encryption.s3.model.GenericServerError; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MetadataUtils { + + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; + } + + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; + } + +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java new file mode 100644 index 00000000..4c772673 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -0,0 +1,55 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.PutObjectInput; +import software.amazon.encryption.s3.model.PutObjectOutput; +import software.amazon.encryption.s3.service.PutObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; + +public class PutObjectOperationImpl implements PutObjectOperation { + + private Map clientCache_; + + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java new file mode 100644 index 00000000..32af5fc1 --- /dev/null +++ b/test-server/java-v3-transition-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import software.amazon.smithy.java.server.Server; +import software.amazon.encryption.s3.service.S3ECTestServer; + +public class S3ECJavaTestServer implements Runnable { + static final URI endpoint = URI.create("http://localhost:8094"); + + public static void main(String[] args) { + new S3ECJavaTestServer().run(); + } + + @Override + public void run() { + // All the S3EC instances live here. + // Obviously this can get messy in a real service. + // Assume that the tests behave and don't induce weird race conditions. + Map clientCache = new ConcurrentHashMap<>(); + + Server server = Server.builder() + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); + System.out.println("Starting server..."); + server.start(); + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + System.out.println("Stopping server..."); + try { + server.shutdown().get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + } +} diff --git a/test-server/java-v4-server/Makefile b/test-server/java-v4-server/Makefile new file mode 100644 index 00000000..922499f7 --- /dev/null +++ b/test-server/java-v4-server/Makefile @@ -0,0 +1,30 @@ +# Makefile for S3 Encryption Client Testing + +.PHONY: start-server stop-server wait-for-server build-s3ec + +PID_FILE := server.pid +PORT := 8090 + +build-s3ec: + @echo "Building S3EC from source..." + cd s3ec-staging && mvn --batch-mode -no-transfer-progress clean compile + cd s3ec-staging && mvn -B -ntp install -DskipTests + @echo "S3EC build completed." + +start-server: build-s3ec + @echo "Starting Java V4 server..." + AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \ + AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \ + AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \ + AWS_REGION="us-west-2" \ + ./gradlew --build-cache --parallel run & echo $$! > $(PID_FILE) + @echo "Java V4 server starting..." + +stop-server: + @if [ -f $(PID_FILE) ]; then \ + kill $$(cat $(PID_FILE)) 2>/dev/null || true; \ + rm $(PID_FILE); \ + fi + +wait-for-server: + $(MAKE) -C .. wait-for-port PORT=$(PORT) diff --git a/test-server/java-v4-server/README.md b/test-server/java-v4-server/README.md new file mode 100644 index 00000000..d011daa2 --- /dev/null +++ b/test-server/java-v4-server/README.md @@ -0,0 +1,23 @@ +# S3EC Java V4 (Improved) Test Server + +This is the Java implementation of the S3ECTestServer framework for S3EC Java V4 (Improved). It provides a server implementation for testing Java S3 Encryption Client V4 (Improved) functionality. + +## Overview + +The S3ECJavaTestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for: + +- Creating S3 Encryption Clients +- Putting objects with encryption +- Getting and decrypting objects + +## Usage + +To run the server: + +```console +gradle run +``` + +This will start the server running on port `8090`. + +The server is used as part of the testing framework to verify cross-language compatibility of the S3 Encryption Client implementations. diff --git a/test-server/java-v4-server/build.gradle.kts b/test-server/java-v4-server/build.gradle.kts new file mode 100644 index 00000000..b874d8e2 --- /dev/null +++ b/test-server/java-v4-server/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") + application +} + +dependencies { + val smithyJavaVersion: String by project + + smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion") + + implementation("software.amazon.smithy:smithy-rules-engine:1.59.0") + implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion") + implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion") + + // S3EC from local Maven repository (installed by mvn install) + implementation("software.amazon.encryption.s3:amazon-s3-encryption-client-java:3.4.0-SNAPSHOT") +} + +// Use that application plugin to start the service via the `run` task. +application { + mainClass = "software.amazon.encryption.s3.S3ECJavaTestServer" +} + +// Add generated Java files to the main sourceSet +afterEvaluate { + val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen") + sourceSets { + main { + java { + srcDir(serverPath) + } + } + } +} + +tasks { + compileJava { + dependsOn(smithyBuild) + } +} + +// Helps Intellij IDE's discover smithy models +sourceSets { + main { + java { + srcDir("../model") + } + } +} + +repositories { + mavenLocal() + mavenCentral() +} diff --git a/test-server/java-v4-server/gradle.properties b/test-server/java-v4-server/gradle.properties new file mode 100644 index 00000000..08afce82 --- /dev/null +++ b/test-server/java-v4-server/gradle.properties @@ -0,0 +1,11 @@ +# Smithy versions +smithyJavaVersion=[0,1] +smithyGradleVersion=1.1.0 +smithyVersion=[1,2] + +# Performance optimization settings +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.workers.max=4 diff --git a/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar b/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e6441136 Binary files /dev/null and b/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.properties b/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a4413138 --- /dev/null +++ b/test-server/java-v4-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/test-server/java-v4-server/gradlew b/test-server/java-v4-server/gradlew new file mode 100755 index 00000000..b740cf13 --- /dev/null +++ b/test-server/java-v4-server/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/test-server/java-v4-server/gradlew.bat b/test-server/java-v4-server/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/test-server/java-v4-server/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test-server/java-v4-server/license.txt b/test-server/java-v4-server/license.txt new file mode 100644 index 00000000..2dd564b3 --- /dev/null +++ b/test-server/java-v4-server/license.txt @@ -0,0 +1,4 @@ +/* + * Example file license header. + * File header line two + */ \ No newline at end of file diff --git a/test-server/java-v4-server/s3ec-staging b/test-server/java-v4-server/s3ec-staging new file mode 160000 index 00000000..ab41a578 --- /dev/null +++ b/test-server/java-v4-server/s3ec-staging @@ -0,0 +1 @@ +Subproject commit ab41a57882f674768c4f528a9069cf69aeb9a53f diff --git a/test-server/java-v4-server/settings.gradle.kts b/test-server/java-v4-server/settings.gradle.kts new file mode 100644 index 00000000..e7c41714 --- /dev/null +++ b/test-server/java-v4-server/settings.gradle.kts @@ -0,0 +1,19 @@ +/** + * Basic usage of generated server stubs. + */ + +pluginManagement { + val smithyGradleVersion: String by settings + + plugins { + id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + } + + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "S3ECJavaTestServer" diff --git a/test-server/java-v4-server/smithy-build.json b/test-server/java-v4-server/smithy-build.json new file mode 100644 index 00000000..a0fcb8e5 --- /dev/null +++ b/test-server/java-v4-server/smithy-build.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "plugins": { + "java-server-codegen": { + "service": "software.amazon.encryption.s3#S3ECTestServer", + "namespace": "software.amazon.encryption.s3", + "headerFile": "license.txt" + } + }, + "sources": ["../model"] +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java new file mode 100644 index 00000000..d992c435 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/CreateClientOperationImpl.java @@ -0,0 +1,109 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.traits.Trait; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.materials.AesKeyring; +import software.amazon.encryption.s3.materials.Keyring; +import software.amazon.encryption.s3.materials.KmsKeyring; +import software.amazon.encryption.s3.materials.PartialRsaKeyPair; +import software.amazon.encryption.s3.materials.RsaKeyring; +import software.amazon.smithy.java.core.schema.Schema; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.CreateClientInput; +import software.amazon.encryption.s3.model.CreateClientOutput; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.KeyMaterial; +import software.amazon.encryption.s3.service.CreateClientOperation; + +import javax.crypto.spec.SecretKeySpec; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CreateClientOperationImpl implements CreateClientOperation { + private Map clientCache_; + + public CreateClientOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + // Copied from S3EC. + private boolean onlyOneNonNull(Object... values) { + boolean haveOneNonNull = false; + for (Object o : values) { + if (o != null) { + if (haveOneNonNull) { + return false; + } + + haveOneNonNull = true; + } + } + + return haveOneNonNull; + } + + @Override + public CreateClientOutput createClient(CreateClientInput input, RequestContext context) { + try { + KeyMaterial key = input.getConfig().getKeyMaterial(); + if (!onlyOneNonNull(key.getAesKey(), key.getKmsKeyId(), key.getRsaKey())) { + throw new RuntimeException("KeyMaterial must be only one, non-null input!"); + } + Keyring keyring; + if (key.getAesKey() != null) { + byte[] keyBytes = new byte[key.getAesKey().remaining()]; + key.getAesKey().get(keyBytes); + keyring = AesKeyring.builder() + .wrappingKey(new SecretKeySpec(keyBytes, "AES")) + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .build(); + } else if (key.getRsaKey() != null) { + try { + byte[] keyBytes = new byte[key.getRsaKey().remaining()]; + key.getRsaKey().get(keyBytes); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + keyring = RsaKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyPair(PartialRsaKeyPair.builder() + .privateKey(keyFactory.generatePrivate(keySpec)).build()) + .build(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException nse) { + throw new RuntimeException(nse); + } + } else if (key.getKmsKeyId() != null) { + keyring = KmsKeyring.builder() + .enableLegacyWrappingAlgorithms(input.getConfig().isEnableLegacyWrappingAlgorithms()) + .wrappingKeyId(key.getKmsKeyId()) + .build(); + } else { + throw new RuntimeException("No KeyMaterial found!"); + } + S3Client s3Client = S3EncryptionClient.builder() + .keyring(keyring) + .build(); + UUID uuid = UUID.randomUUID(); + String uuidString = uuid.toString(); + clientCache_.put(uuidString, s3Client); + return CreateClientOutput.builder() + .clientId(uuidString) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java new file mode 100644 index 00000000..e7c5493f --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/GetObjectOperationImpl.java @@ -0,0 +1,72 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.GetObjectInput; +import software.amazon.encryption.s3.model.GetObjectOutput; +import software.amazon.encryption.s3.model.S3EncryptionClientError; +import software.amazon.encryption.s3.service.GetObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; +import static software.amazon.encryption.s3.MetadataUtils.metadataMapToList; + +public class GetObjectOperationImpl implements GetObjectOperation { + private Map clientCache_; + public GetObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + @Override + public GetObjectOutput getObject(GetObjectInput input, RequestContext context) { + try { + S3Client s3Client = clientCache_.get(input.getClientID()); + Map ecMap = metadataListToMap(input.getMetadata()); + + try { + ResponseBytes resp = s3Client.getObjectAsBytes(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(ecMap))); + + List mdAsList = metadataMapToList(resp.response().metadata()); + // Can't use asBB else it gets mad bc cant access backing array + ByteBuffer bb = ByteBuffer.wrap(resp.asByteArray()); + GetObjectOutput output = GetObjectOutput.builder() + .body(bb) + .metadata(mdAsList) + .build(); + return output; + } catch (S3EncryptionClientException s3EncryptionClientException) { + // Modeled exceptions MUST be returned as such + StringWriter sw = new StringWriter(); + s3EncryptionClientException.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw S3EncryptionClientError.builder() + .message(stackTrace) + .build(); + } + } catch (Exception e) { + // Don't wrap modeled errors + if (e instanceof S3EncryptionClientError) { + throw e; + } + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java new file mode 100644 index 00000000..036289ec --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/MetadataUtils.java @@ -0,0 +1,43 @@ +package software.amazon.encryption.s3; + +import software.amazon.encryption.s3.model.GenericServerError; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MetadataUtils { + + /** + * Annoyingly, Smithy doesn't provide an interface for map types + * in HTTP headers, so we have to do the serde ourselves + */ + public static List metadataMapToList(Map md) { + List mdAsList = new ArrayList<>(md.size()); + for (Map.Entry keyValue : md.entrySet()) { + mdAsList.add("[" + keyValue.getKey() + "]:[" + keyValue.getValue() + "]"); + } + return mdAsList; + } + + public static Map metadataListToMap(List mdList) { + Map md = new HashMap<>(); + for (String entry : mdList) { + // Split on "]:[" to separate key and value + String[] parts = entry.split("]:\\["); + if (parts.length == 2) { + // Remove remaining brackets from start and end + String key = parts[0].substring(1); + String value = parts[1].substring(0, parts[1].length() - 1); + md.put(key, value); + } else { + throw GenericServerError.builder() + .message("Malformed metadata list entry: " + entry) + .build(); + } + } + return md; + } + +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java new file mode 100644 index 00000000..4c772673 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/PutObjectOperationImpl.java @@ -0,0 +1,55 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.smithy.java.server.RequestContext; +import software.amazon.encryption.s3.model.GenericServerError; +import software.amazon.encryption.s3.model.PutObjectInput; +import software.amazon.encryption.s3.model.PutObjectOutput; +import software.amazon.encryption.s3.service.PutObjectOperation; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Map; +import java.util.stream.Collectors; + +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.MetadataUtils.metadataListToMap; + +public class PutObjectOperationImpl implements PutObjectOperation { + + private Map clientCache_; + + public PutObjectOperationImpl(Map clientCache) { + clientCache_ = clientCache; + } + + @Override + public PutObjectOutput putObject(PutObjectInput input, RequestContext context) { + try { + final Map metadata = metadataListToMap(input.getMetadata()); + S3Client s3Client = clientCache_.get(input.getClientID()); + s3Client.putObject(builder -> builder + .bucket(input.getBucket()) + .key(input.getKey()) + .overrideConfiguration(withAdditionalConfiguration(metadata)), + RequestBody.fromByteBuffer(input.getBody()) + ); + // The real S3 doesn't provide bucket/key/metadata, so Test doesn't need to either, but we do anyway + return PutObjectOutput.builder() + .bucket(input.getBucket()) + .key(input.getKey()) + .metadata(input.getMetadata()) + .build(); + } catch (Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + throw GenericServerError.builder() + .message(stackTrace) + .build(); + } + } +} diff --git a/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java new file mode 100644 index 00000000..7b73ffd1 --- /dev/null +++ b/test-server/java-v4-server/src/main/java/software/amazon/encryption/s3/S3ECJavaTestServer.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.encryption.s3; + +import software.amazon.awssdk.services.s3.S3Client; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import software.amazon.smithy.java.server.Server; +import software.amazon.encryption.s3.service.S3ECTestServer; + +public class S3ECJavaTestServer implements Runnable { + static final URI endpoint = URI.create("http://localhost:8090"); + + public static void main(String[] args) { + new S3ECJavaTestServer().run(); + } + + @Override + public void run() { + // All the S3EC instances live here. + // Obviously this can get messy in a real service. + // Assume that the tests behave and don't induce weird race conditions. + Map clientCache = new ConcurrentHashMap<>(); + + Server server = Server.builder() + .endpoints(endpoint) + .addService( + S3ECTestServer.builder() + .addCreateClientOperation(new CreateClientOperationImpl(clientCache)) + .addGetObjectOperation(new GetObjectOperationImpl(clientCache)) + .addPutObjectOperation(new PutObjectOperationImpl(clientCache)) + .build()) + .build(); + System.out.println("Starting server..."); + server.start(); + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + System.out.println("Stopping server..."); + try { + server.shutdown().get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + } +}