diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index 66c9dac2..b13d5d8a 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -49,6 +49,7 @@ jobs: - { name: JavalinMySQLKotlin, test_file: end2end/javalin_mysql_kotlin.py, db: mysql_database } - { name: SpringBoot2.7Postgres, test_file: end2end/spring_boot_2.7_postgres.py, db: postgres_database } - { name: SpringBootHyperSQL, test_file: end2end/spring_boot_hypersql.py, db: "" } + - { name: SpringBoot4HyperSQL, test_file: end2end/spring_boot_4_hypersql.py, db: "" } java-version: [17, 18, 19, 20, 21, 24, 25] distribution: ['adopt', 'corretto', 'oracle'] exclude: diff --git a/agent/src/main/java/dev/aikido/agent/Wrappers.java b/agent/src/main/java/dev/aikido/agent/Wrappers.java index a71c13b2..b54123f7 100644 --- a/agent/src/main/java/dev/aikido/agent/Wrappers.java +++ b/agent/src/main/java/dev/aikido/agent/Wrappers.java @@ -9,6 +9,7 @@ import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper; import dev.aikido.agent.wrappers.spring.SpringControllerWrapper; import dev.aikido.agent.wrappers.spring.SpringMVCJakartaWrapper; +import dev.aikido.agent.wrappers.spring.SpringMVCDispatcherWrapper; import java.util.Arrays; import java.util.List; @@ -18,6 +19,7 @@ private Wrappers() {} public static final List WRAPPERS = Arrays.asList( new PostgresWrapper(), new SpringMVCJakartaWrapper(), + new SpringMVCDispatcherWrapper(), new SpringMVCJavaxWrapper(), new SpringWebfluxWrapper(), new SpringControllerWrapper(), diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCDispatcherWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCDispatcherWrapper.java new file mode 100644 index 00000000..93b99a58 --- /dev/null +++ b/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCDispatcherWrapper.java @@ -0,0 +1,107 @@ +package dev.aikido.agent.wrappers.spring; + +import dev.aikido.agent.wrappers.Wrapper; +import dev.aikido.agent_api.collectors.WebRequestCollector; +import dev.aikido.agent_api.collectors.WebResponseCollector; +import dev.aikido.agent_api.context.ContextObject; +import dev.aikido.agent_api.context.SpringMVCContextObject; +import dev.aikido.agent_api.helpers.logging.LogManager; +import dev.aikido.agent_api.helpers.logging.Logger; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.reflect.Executable; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; + +import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +// Fallback context creator for Spring MVC: wraps FrameworkServlet#processRequest (runs for every +// DispatcherServlet request) and creates the context when RequestContextFilter is absent from the chain. +public class SpringMVCDispatcherWrapper implements Wrapper { + public static final Logger logger = LogManager.getLogger(SpringMVCDispatcherWrapper.class); + + @Override + public String getName() { + return SpringMVCDispatcherAdvice.class.getName(); + } + + @Override + public ElementMatcher getMatcher() { + return ElementMatchers.nameContainsIgnoreCase("processRequest") + .and(takesArgument(0, nameContains("jakarta"))) + .and(takesArgument(1, nameContains("jakarta"))); + } + + @Override + public ElementMatcher getTypeMatcher() { + return nameContains("org.springframework.web.servlet.FrameworkServlet"); + } + + public static class SpringMVCDispatcherAdvice { + public record SkipOnWrapper(HttpServletResponse response) {} + + @Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class) + public static Object interceptOnEnter( + @Advice.Origin Executable method, + @Advice.Argument(value = 0, typing = DYNAMIC, optional = true) HttpServletRequest request, + @Advice.Argument(value = 1, typing = DYNAMIC, optional = true) HttpServletResponse response) throws Throwable { + if (request == null) { + return null; + } + // Per-request marker (not Context.get()) since pooled threads keep a stale context between requests. + if (request.getAttribute("dev.aikido.zen.springContextCreated") != null) { + return null; + } + HashMap> headersMap = new HashMap<>(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames != null && headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + Enumeration headerValue = request.getHeaders(headerName); + headersMap.put(headerName, headerValue); + } + HashMap> cookiesMap = new HashMap<>(); + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (!cookiesMap.containsKey(cookie.getName())) { + cookiesMap.put(cookie.getName(), new ArrayList<>()); + } + cookiesMap.get(cookie.getName()).add(cookie.getValue()); + } + } + + ContextObject contextObject = new SpringMVCContextObject( + request.getMethod(), request.getRequestURL(), request.getRemoteAddr(), + request.getParameterMap(), cookiesMap, headersMap, request.getQueryString() + ); + + request.setAttribute("dev.aikido.zen.springContextCreated", Boolean.TRUE); + WebRequestCollector.Res res = WebRequestCollector.report(contextObject); + if (res != null && response != null) { + response.setStatus(res.status()); + response.setContentType("text/plain"); + response.getWriter().write(res.msg()); + return new SkipOnWrapper(response); + } + return response; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void interceptOnExit(@Advice.Enter Object response) { + if (response instanceof HttpServletResponse httpServletResponse) { + WebResponseCollector.report(httpServletResponse.getStatus()); + } + } + } +} diff --git a/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java b/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java index d1173350..f133501b 100644 --- a/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java +++ b/agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java @@ -90,6 +90,9 @@ public static Object interceptOnEnter( request.getParameterMap(), cookiesMap, headersMap, request.getQueryString() ); + // Marker read by SpringMVCDispatcherWrapper so the fallback does not re-create this context. + request.setAttribute("dev.aikido.zen.springContextCreated", Boolean.TRUE); + // Write a new response: WebRequestCollector.Res res = WebRequestCollector.report(contextObject); if (res != null) { diff --git a/end2end/spring_boot_4_hypersql.py b/end2end/spring_boot_4_hypersql.py new file mode 100644 index 00000000..256d193d --- /dev/null +++ b/end2end/spring_boot_4_hypersql.py @@ -0,0 +1,10 @@ +from utils import App, Request + +spring_boot_4_hsql_app = App(8110) + +spring_boot_4_hsql_app.add_payload("sql", + safe_request=Request("/api/pets/create", body={"name": "Bobby"}), + unsafe_request=Request("/api/pets/create", body={"name": "Malicious Pet', 'Gru from the Minions') -- "}) +) + +spring_boot_4_hsql_app.test_all_payloads() diff --git a/sample-apps/SpringBoot4HyperSQL/.gitignore b/sample-apps/SpringBoot4HyperSQL/.gitignore new file mode 100644 index 00000000..29105379 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/.gitignore @@ -0,0 +1,2 @@ +**output**.log +dd-java-agent** diff --git a/sample-apps/SpringBoot4HyperSQL/Makefile b/sample-apps/SpringBoot4HyperSQL/Makefile new file mode 100644 index 00000000..ae7e4cd2 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/Makefile @@ -0,0 +1,38 @@ +# Define variables +GRADLEW = ./gradlew +JAR_FILE = build/libs/demo-0.0.1-SNAPSHOT.jar +JAVA_AGENT = ../../dist/agent.jar + +# Default target +.PHONY: all +all: build + +# Build the project +.PHONY: build +build: + @echo "Building the project..." + chmod +x $(GRADLEW) + $(GRADLEW) build + +# Run the application with the Java agent +.PHONY: run +run: build + @echo "Running SpringBoot4HyperSQL with Zen & ENV (http://localhost:8110)" + AIKIDO_LOG_LEVEL="error" \ + AIKIDO_TOKEN="token" \ + AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/realtime" \ + AIKIDO_ENDPOINT="http://localhost:5000" \ + AIKIDO_BLOCK=1 \ + java -javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8110 + +# Run the application without Zen +.PHONY: runWithoutZen +runWithoutZen: build + @echo "Running SpringBoot4HyperSQL without Zen & ENV (http://localhost:8111)" + AIKIDO_TOKEN="random-invalid-token" java -jar $(JAR_FILE) --server.port=8111 + +# Clean the project +.PHONY: clean +clean: + @echo "Cleaning the project..." + $(GRADLEW) clean diff --git a/sample-apps/SpringBoot4HyperSQL/README.md b/sample-apps/SpringBoot4HyperSQL/README.md new file mode 100644 index 00000000..e460064f --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/README.md @@ -0,0 +1,7 @@ +# SpringBoot 4 + HyperSQL vulnerable sample app + +Spring Boot 4 (Spring Framework 7, Jakarta) app on HyperSQL. It registers a `RequestContextListener` +bean, which makes `WebMvcAutoConfiguration` skip the auto `RequestContextFilter`. Zen must then create +the Spring MVC context from `FrameworkServlet#processRequest` instead of the filter. + +- Inserting a malicious dog : `Malicious Pet', 'Gru from the Minions') -- ` diff --git a/sample-apps/SpringBoot4HyperSQL/build.gradle b/sample-apps/SpringBoot4HyperSQL/build.gradle new file mode 100644 index 00000000..65dbe5bc --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '4.0.7' + id 'io.spring.dependency-management' version '1.1.6' +} + +java { + sourceCompatibility = '17' + targetCompatibility = '17' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.hsqldb:hsqldb:2.7.2' +} diff --git a/sample-apps/SpringBoot4HyperSQL/gradle/gradle-daemon-jvm.properties b/sample-apps/SpringBoot4HyperSQL/gradle/gradle-daemon-jvm.properties new file mode 100644 index 00000000..63e5bbdf --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,2 @@ +#This file is generated by updateDaemonJvm +toolchainVersion=21 diff --git a/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.jar b/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.properties b/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a351597e --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sample-apps/SpringBoot4HyperSQL/gradlew b/sample-apps/SpringBoot4HyperSQL/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/gradlew @@ -0,0 +1,252 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || 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/sample-apps/SpringBoot4HyperSQL/gradlew.bat b/sample-apps/SpringBoot4HyperSQL/gradlew.bat new file mode 100644 index 00000000..9d21a218 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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/sample-apps/SpringBoot4HyperSQL/settings.gradle b/sample-apps/SpringBoot4HyperSQL/settings.gradle new file mode 100644 index 00000000..0a383dd8 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'demo' diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseHelper.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseHelper.java new file mode 100644 index 00000000..0816869e --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseHelper.java @@ -0,0 +1,72 @@ +package com.example.demo; + +import javax.sql.DataSource; +import org.hsqldb.jdbc.JDBCDataSource; +import com.example.demo.models.Pet; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +public class DatabaseHelper { + static DataSource createDataSource() { + JDBCDataSource dataSource = new JDBCDataSource(); + + // For in-memory database (data is lost when the application stops) + dataSource.setURL("jdbc:hsqldb:mem:mydatabase"); + dataSource.setUser("SA"); + dataSource.setPassword(""); + return dataSource; + } + + public static ArrayList getAllPets() { + ArrayList pets = new ArrayList<>(); + DataSource db = createDataSource(); + try { + Connection conn = db.getConnection(); + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM pets"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Integer id = rs.getInt("pet_id"); + String name = rs.getString("pet_name"); + String owner = rs.getString("owner"); + pets.add(new Pet(id, name, owner)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return pets; + } + public static Pet getPetById(Integer id) { + ArrayList pets = new ArrayList<>(); + DataSource db = createDataSource(); + try { + Connection conn = db.getConnection(); + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM pets WHERE pet_id=?"); + stmt.setInt(1, id); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Integer pet_id = rs.getInt("pet_id"); + String name = rs.getString("pet_name"); + String owner = rs.getString("owner"); + return new Pet(pet_id, name, owner); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return new Pet(0, "Unknown", "Unknown"); + } + public static Integer createPetByName(String pet_name) { + String sql = "INSERT INTO pets (pet_name, owner) VALUES ('" + pet_name + "', 'Aikido Security')"; + DataSource db = createDataSource(); + try { + Connection conn = db.getConnection(); + PreparedStatement insertStmt = conn.prepareStatement(sql); + return insertStmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } +} diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseInitializer.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseInitializer.java new file mode 100644 index 00000000..12998c47 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DatabaseInitializer.java @@ -0,0 +1,22 @@ +package com.example.demo; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class DatabaseInitializer { + public static void initialize() throws SQLException { + DataSource db = DatabaseHelper.createDataSource(); + try (Connection conn = db.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS pets (" + + "pet_id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1), " + + "pet_name VARCHAR(255) NOT NULL, " + + "owner VARCHAR(255) NOT NULL, " + + "PRIMARY KEY (pet_id)" + + ")")) { + stmt.execute(); + } + } +} diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DemoApplication.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 00000000..bf3e939a --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,28 @@ +package com.example.demo; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.web.context.request.RequestContextListener; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + + // Publishing request context via a RequestContextListener makes WebMvcAutoConfiguration skip the + // auto RequestContextFilter, so Zen must fall back to FrameworkServlet#processRequest for the context. + @Bean + public RequestContextListener requestContextListener() { + return new RequestContextListener(); + } + + @Bean + public CommandLineRunner commandLineRunner(ApplicationContext ctx) { + return args -> DatabaseInitializer.initialize(); + } +} diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/HomeController.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/HomeController.java new file mode 100644 index 00000000..ed7cc80f --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/HomeController.java @@ -0,0 +1,12 @@ +package com.example.demo; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HomeController { + @GetMapping("/") + public String home() { + return "OK"; + } +} diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/PetsController.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/PetsController.java new file mode 100644 index 00000000..33ab5f26 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/PetsController.java @@ -0,0 +1,31 @@ +package com.example.demo; + +import com.example.demo.models.Pet; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; + +@RestController +@RequestMapping("/api/pets") // Base URL for all routes in this controller +public class PetsController { + @GetMapping("/") + public ArrayList index() { + return DatabaseHelper.getAllPets(); + } + + @GetMapping("/{id}") + public Pet pet(@PathVariable("id") Integer id) { + return DatabaseHelper.getPetById(id); + } + + private record PetCreate(String name) {} + public record Rows(Integer rows) {} + @PostMapping(path = "/create", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public Rows create(@RequestBody PetCreate pet_data) { + Integer rowsCreated = DatabaseHelper.createPetByName(pet_data.name); + return new Rows(rowsCreated); + } +} \ No newline at end of file diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/models/Pet.java b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/models/Pet.java new file mode 100644 index 00000000..390e8197 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/java/com/example/demo/models/Pet.java @@ -0,0 +1,12 @@ +package com.example.demo.models; + +/* +Creating this table using SQL for HSQLDB: +CREATE TABLE pets ( + pet_id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + name VARCHAR(255) NOT NULL, + owner VARCHAR(255) NOT NULL, + PRIMARY KEY (pet_id) +) +*/ +public record Pet(Integer pet_id, String name, String owner) {} diff --git a/sample-apps/SpringBoot4HyperSQL/src/main/resources/application.properties b/sample-apps/SpringBoot4HyperSQL/src/main/resources/application.properties new file mode 100644 index 00000000..14feffa1 --- /dev/null +++ b/sample-apps/SpringBoot4HyperSQL/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.application.name=demo +server.tomcat.threads.max=6 \ No newline at end of file