From 0808f6288ec71fdbacc4cc898277189b9100ef89 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 15 May 2019 12:29:36 -0700 Subject: [PATCH 01/63] added gradle script, set up dependencies --- .gitignore | 5 + build.gradle | 67 +++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++++++++++++++++ gradlew.bat | 84 +++++++++++ settings.gradle | 10 ++ src/main/java/JobServ/App.java | 14 ++ src/test/java/JobServ/AppTest.java | 14 ++ 9 files changed, 371 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/JobServ/App.java create mode 100644 src/test/java/JobServ/AppTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..13646db --- /dev/null +++ b/build.gradle @@ -0,0 +1,67 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java project to get you started. + * For more details take a look at the Java Quickstart chapter in the Gradle + * User Manual available at https://docs.gradle.org/5.2.1/userguide/tutorial_java_projects.html + */ + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' + } +} + +plugins { + id 'java' + id 'application' +} + +apply plugin: 'com.google.protobuf' + +repositories { + // Use jcenter for resolving your dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() +} + +dependencies { + // This dependency is found on compile classpath of this component and consumers. + implementation 'com.google.guava:guava:27.0.1-jre' + + // Use JUnit test framework + testImplementation 'junit:junit:4.12' + + // grpc stuff + compile 'io.grpc:grpc-netty-shaded:1.20.0' + compile 'io.grpc:grpc-protobuf:1.20.0' + compile 'io.grpc:grpc-stub:1.20.0' + compile 'io.netty:netty-tcnative-boringssl-static:2.0.22.Final' +} + +// Define the main class for the application +mainClassName = 'JobServ.App' + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.7.1" + } + + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.20.0' + } + } + + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem 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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..cf58543 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/5.2.1/userguide/multi_project_builds.html + */ + +rootProject.name = 'JobServ' diff --git a/src/main/java/JobServ/App.java b/src/main/java/JobServ/App.java new file mode 100644 index 0000000..ff0acde --- /dev/null +++ b/src/main/java/JobServ/App.java @@ -0,0 +1,14 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package JobServ; + +public class App { + public String getGreeting() { + return "Hello world."; + } + + public static void main(String[] args) { + System.out.println(new App().getGreeting()); + } +} diff --git a/src/test/java/JobServ/AppTest.java b/src/test/java/JobServ/AppTest.java new file mode 100644 index 0000000..6a7ef0a --- /dev/null +++ b/src/test/java/JobServ/AppTest.java @@ -0,0 +1,14 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package JobServ; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class AppTest { + @Test public void testAppHasAGreeting() { + App classUnderTest = new App(); + assertNotNull("app should have a greeting", classUnderTest.getGreeting()); + } +} From 26fee4a242b90520a09a9476c1693295596e128c Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 15 May 2019 15:54:52 -0700 Subject: [PATCH 02/63] add protobuf definitions --- src/main/proto/jobserv.proto | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/proto/jobserv.proto diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto new file mode 100644 index 0000000..16009aa --- /dev/null +++ b/src/main/proto/jobserv.proto @@ -0,0 +1,33 @@ +syntax = "proto3" + +option java_multiple_files = true; +option java_package = "com.ajhahn.jobserv" +option java_outer_classname = "JobServProto" +option objc_class_prefix = "JSV" + +package jobserv; + +service jobserv { + rpc getStatusDetail (RequestMessage) returns (JobStatusMessage) {} + rpc getJobOutput (RequestMessage) returns (OutputMessage) {} + rpc makeNewJob (NewJobMessage) returns (JobStatusMessage) {} +} + +message RequestMessage { + int32 PID = 1; + int32 RequestType = 2; +} + +message NewJobMessage { + string Command = 1; +} + +message JobStatusMessage { + int32 PID = 1; + ProcessStatus = 2; +} + +message OutputMessage { + int32 PID = 1; + string ProcessOutput = 2; +} \ No newline at end of file From dbcbd4dbad1489f62e8bf941e734f1e53c523a44 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 15 May 2019 16:08:44 -0700 Subject: [PATCH 03/63] fixed typos in previous commit, added editor swapfiles to gitignore --- .gitignore | 4 ++++ src/main/proto/jobserv.proto | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 1b6985c..f2d1c84 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ # Ignore Gradle build output directory build + +# Ignore emacs swapfiles +\#* +.\#* \ No newline at end of file diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index 16009aa..8d6632a 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -1,9 +1,9 @@ -syntax = "proto3" +syntax = "proto3"; option java_multiple_files = true; -option java_package = "com.ajhahn.jobserv" -option java_outer_classname = "JobServProto" -option objc_class_prefix = "JSV" +option java_package = "com.ajhahn.jobserv"; +option java_outer_classname = "JobServProto"; +option objc_class_prefix = "JSV"; package jobserv; @@ -24,7 +24,7 @@ message NewJobMessage { message JobStatusMessage { int32 PID = 1; - ProcessStatus = 2; + string ProcessStatus = 2; } message OutputMessage { From ee2b7f0b4cb8b7f1160b6ae3a14f0dc08a0808c6 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 16 May 2019 14:49:47 -0700 Subject: [PATCH 04/63] implemented test server --- src/main/java/JobServ/App.java | 14 ---- src/main/java/JobServ/JobServClient.java | 0 src/main/java/JobServ/JobServServer.java | 94 ++++++++++++++++++++++++ src/main/proto/jobserv.proto | 6 +- 4 files changed, 97 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/JobServ/App.java create mode 100644 src/main/java/JobServ/JobServClient.java create mode 100644 src/main/java/JobServ/JobServServer.java diff --git a/src/main/java/JobServ/App.java b/src/main/java/JobServ/App.java deleted file mode 100644 index ff0acde..0000000 --- a/src/main/java/JobServ/App.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package JobServ; - -public class App { - public String getGreeting() { - return "Hello world."; - } - - public static void main(String[] args) { - System.out.println(new App().getGreeting()); - } -} diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java new file mode 100644 index 0000000..49bd771 --- /dev/null +++ b/src/main/java/JobServ/JobServServer.java @@ -0,0 +1,94 @@ +package JobServ; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; + +import java.io.IOException; +import java.util.logging.Logger; + +public class JobServServer { + private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); + + private Server server; + + // starts the GRPC API Server + private void start() throws IOException { + // TODO: this should be passed in from a configuration manager + int port = 8448; + server = ServerBUilder.forPort(port) + .addService(new JobServImpl()) + .build() + .start(); + logger.info("Server initialized without tls"); + Runtime.getRuntime().addShutdownHook(new thread() { + @Override + public void run() { + // JVM shutdown might break logger functionality + // so investigate this.... + logger.info("Shutting down server"); + JobServServer.this.stop(); + } + }); + } + + private void stop() { + if (server != null) { + server.shutdown(); + } + } + + // spinlock for main() + private void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } + + // Main funciton. starts GRPC server and spins until server is shutdown + public static void main(String[] args) throws IOException, InterruptedException { + final JobServServer server = new JobServServer(); + server.start(); + server.blockUntilShutdown(); + } + + // wrap around stub code generated by GRPC + static class JobServImpl extends JobServGrpc.JobServImplBase { + + @Override + public void getStatusDetail(RequestMessage request, + StreamObserver responder) { + // TODO: Implement job controller, check request type, do the thing + JobStatusMessage reply = JobStatusMessage.newBuilder() + .setPID(-1) + .setProcessStatus(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void getJobOutput(RequestMessage request, + StreamObserver responder) { + // TODO: Implement job controller, get output, do the thing + OutputMessage reply = OutputMessage.newBuilder() + .setPID(-1) + .setProcessOutput("Hello World!") + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void makeNewJob(NewJobMessage request, + StreamObserver responder) { + // TODO: Implement job controller, Start Job, do the thing + JobStatusMessage reply = JobStatusMessage.newBuilder() + .setPID(-1) + .setProcessStatus(-1) + .build(): + responder.onNext(reply); + responder.onCompleted(); + } + } +} diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index 8d6632a..2256f87 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -23,11 +23,11 @@ message NewJobMessage { } message JobStatusMessage { - int32 PID = 1; - string ProcessStatus = 2; + int32 PID = 1; // linux PIDs, by default, go up to 2^22 + int32 ProcessStatus = 2; // TODO: figure out smallest possible data size for this } message OutputMessage { int32 PID = 1; string ProcessOutput = 2; -} \ No newline at end of file +} From d6e73cf7adb8d29b9f9905880a972b491d9e17a9 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 01:28:26 -0700 Subject: [PATCH 05/63] Client builds --- build.gradle | 18 +- gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 5 - gradlew | 172 ------------------ gradlew.bat | 84 --------- settings.gradle | 10 -- src/main/java/JobServ/JobServClient.java | 216 +++++++++++++++++++++++ src/main/java/JobServ/JobServServer.java | 125 +++++++------ src/main/proto/jobserv.proto | 9 +- src/test/java/JobServ/AppTest.java | 14 -- 10 files changed, 297 insertions(+), 356 deletions(-) delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat delete mode 100644 settings.gradle delete mode 100644 src/test/java/JobServ/AppTest.java diff --git a/build.gradle b/build.gradle index 13646db..acfcbd6 100644 --- a/build.gradle +++ b/build.gradle @@ -18,10 +18,13 @@ buildscript { plugins { id 'java' + id 'com.google.protobuf' version '0.8.8' id 'application' } -apply plugin: 'com.google.protobuf' +def grpcVersion = '1.22.0-SNAPSHOT' +def protobufVersion = '3.7.1' +def protocVersion = protobufVersion repositories { // Use jcenter for resolving your dependencies. @@ -36,6 +39,9 @@ dependencies { // Use JUnit test framework testImplementation 'junit:junit:4.12' + // Used by GRPC generated code + compile 'org.glassfish:javax.annotation:10.0-b28' + // grpc stuff compile 'io.grpc:grpc-netty-shaded:1.20.0' compile 'io.grpc:grpc-protobuf:1.20.0' @@ -44,7 +50,7 @@ dependencies { } // Define the main class for the application -mainClassName = 'JobServ.App' +mainClassName = 'JobServ.JobServClient' protobuf { protoc { @@ -64,4 +70,10 @@ protobuf { } } - +apply plugin: 'application' +mainClassName = 'JobServ.JobServClient' +jar { + manifest { + attributes('Main-Class' : mainClassName) + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 87b738cbd051603d91cc39de6cb000dd98fe6b02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55190 zcmafaW0WS*vSoFbZQHhO+s0S6%`V%vZQJa!ZQHKus_B{g-pt%P_q|ywBQt-*Stldc z$+IJ3?^KWm27v+sf`9-50uuadKtMnL*BJ;1^6ynvR7H?hQcjE>7)art9Bu0Pcm@7C z@c%WG|JzYkP)<@zR9S^iR_sA`azaL$mTnGKnwDyMa;8yL_0^>Ba^)phg0L5rOPTbm7g*YIRLg-2^{qe^`rb!2KqS zk~5wEJtTdD?)3+}=eby3x6%i)sb+m??NHC^u=tcG8p$TzB<;FL(WrZGV&cDQb?O0GMe6PBV=V z?tTO*5_HTW$xea!nkc~Cnx#cL_rrUGWPRa6l+A{aiMY=<0@8y5OC#UcGeE#I>nWh}`#M#kIn-$A;q@u-p71b#hcSItS!IPw?>8 zvzb|?@Ahb22L(O4#2Sre&l9H(@TGT>#Py)D&eW-LNb!=S;I`ZQ{w;MaHW z#to!~TVLgho_Pm%zq@o{K3Xq?I|MVuVSl^QHnT~sHlrVxgsqD-+YD?Nz9@HA<;x2AQjxP)r6Femg+LJ-*)k%EZ}TTRw->5xOY z9#zKJqjZgC47@AFdk1$W+KhTQJKn7e>A&?@-YOy!v_(}GyV@9G#I?bsuto4JEp;5|N{orxi_?vTI4UF0HYcA( zKyGZ4<7Fk?&LZMQb6k10N%E*$gr#T&HsY4SPQ?yerqRz5c?5P$@6dlD6UQwZJ*Je9 z7n-@7!(OVdU-mg@5$D+R%gt82Lt%&n6Yr4=|q>XT%&^z_D*f*ug8N6w$`woqeS-+#RAOfSY&Rz z?1qYa5xi(7eTCrzCFJfCxc%j{J}6#)3^*VRKF;w+`|1n;Xaojr2DI{!<3CaP`#tXs z*`pBQ5k@JLKuCmovFDqh_`Q;+^@t_;SDm29 zCNSdWXbV?9;D4VcoV`FZ9Ggrr$i<&#Dx3W=8>bSQIU_%vf)#(M2Kd3=rN@^d=QAtC zI-iQ;;GMk|&A++W5#hK28W(YqN%?!yuW8(|Cf`@FOW5QbX|`97fxmV;uXvPCqxBD zJ9iI37iV)5TW1R+fV16y;6}2tt~|0J3U4E=wQh@sx{c_eu)t=4Yoz|%Vp<#)Qlh1V z0@C2ZtlT>5gdB6W)_bhXtcZS)`9A!uIOa`K04$5>3&8An+i9BD&GvZZ=7#^r=BN=k za+=Go;qr(M)B~KYAz|<^O3LJON}$Q6Yuqn8qu~+UkUKK~&iM%pB!BO49L+?AL7N7o z(OpM(C-EY753=G=WwJHE`h*lNLMNP^c^bBk@5MyP5{v7x>GNWH>QSgTe5 z!*GPkQ(lcbEs~)4ovCu!Zt&$${9$u(<4@9%@{U<-ksAqB?6F`bQ;o-mvjr)Jn7F&j$@`il1Mf+-HdBs<-`1FahTxmPMMI)@OtI&^mtijW6zGZ67O$UOv1Jj z;a3gmw~t|LjPkW3!EZ=)lLUhFzvO;Yvj9g`8hm%6u`;cuek_b-c$wS_0M4-N<@3l|88 z@V{Sd|M;4+H6guqMm4|v=C6B7mlpP(+It%0E;W`dxMOf9!jYwWj3*MRk`KpS_jx4c z=hrKBkFK;gq@;wUV2eqE3R$M+iUc+UD0iEl#-rECK+XmH9hLKrC={j@uF=f3UiceB zU5l$FF7#RKjx+6!JHMG5-!@zI-eG=a-!Bs^AFKqN_M26%cIIcSs61R$yuq@5a3c3& z4%zLs!g}+C5%`ja?F`?5-og0lv-;(^e<`r~p$x%&*89_Aye1N)9LNVk?9BwY$Y$$F^!JQAjBJvywXAesj7lTZ)rXuxv(FFNZVknJha99lN=^h`J2> zl5=~(tKwvHHvh|9-41@OV`c;Ws--PE%{7d2sLNbDp;A6_Ka6epzOSFdqb zBa0m3j~bT*q1lslHsHqaHIP%DF&-XMpCRL(v;MV#*>mB^&)a=HfLI7efblG z(@hzN`|n+oH9;qBklb=d^S0joHCsArnR1-h{*dIUThik>ot^!6YCNjg;J_i3h6Rl0ji)* zo(tQ~>xB!rUJ(nZjCA^%X;)H{@>uhR5|xBDA=d21p@iJ!cH?+%U|VSh2S4@gv`^)^ zNKD6YlVo$%b4W^}Rw>P1YJ|fTb$_(7C;hH+ z1XAMPb6*p^h8)e5nNPKfeAO}Ik+ZN_`NrADeeJOq4Ak;sD~ zTe77no{Ztdox56Xi4UE6S7wRVxJzWxKj;B%v7|FZ3cV9MdfFp7lWCi+W{}UqekdpH zdO#eoOuB3Fu!DU`ErfeoZWJbWtRXUeBzi zBTF-AI7yMC^ntG+8%mn(I6Dw}3xK8v#Ly{3w3_E?J4(Q5JBq~I>u3!CNp~Ekk&YH` z#383VO4O42NNtcGkr*K<+wYZ>@|sP?`AQcs5oqX@-EIqgK@Pmp5~p6O6qy4ml~N{D z{=jQ7k(9!CM3N3Vt|u@%ssTw~r~Z(}QvlROAkQQ?r8OQ3F0D$aGLh zny+uGnH5muJ<67Z=8uilKvGuANrg@s3Vu_lU2ajb?rIhuOd^E@l!Kl0hYIxOP1B~Q zggUmXbh$bKL~YQ#!4fos9UUVG#}HN$lIkM<1OkU@r>$7DYYe37cXYwfK@vrHwm;pg zbh(hEU|8{*d$q7LUm+x&`S@VbW*&p-sWrplWnRM|I{P;I;%U`WmYUCeJhYc|>5?&& zj}@n}w~Oo=l}iwvi7K6)osqa;M8>fRe}>^;bLBrgA;r^ZGgY@IC^ioRmnE&H4)UV5 zO{7egQ7sBAdoqGsso5q4R(4$4Tjm&&C|7Huz&5B0wXoJzZzNc5Bt)=SOI|H}+fbit z-PiF5(NHSy>4HPMrNc@SuEMDuKYMQ--G+qeUPqO_9mOsg%1EHpqoX^yNd~~kbo`cH zlV0iAkBFTn;rVb>EK^V6?T~t~3vm;csx+lUh_%ROFPy0(omy7+_wYjN!VRDtwDu^h4n|xpAMsLepm% zggvs;v8+isCW`>BckRz1MQ=l>K6k^DdT`~sDXTWQ<~+JtY;I~I>8XsAq3yXgxe>`O zZdF*{9@Z|YtS$QrVaB!8&`&^W->_O&-JXn1n&~}o3Z7FL1QE5R*W2W@=u|w~7%EeC1aRfGtJWxImfY-D3t!!nBkWM> zafu>^Lz-ONgT6ExjV4WhN!v~u{lt2-QBN&UxwnvdH|I%LS|J-D;o>@@sA62@&yew0 z)58~JSZP!(lX;da!3`d)D1+;K9!lyNlkF|n(UduR-%g>#{`pvrD^ClddhJyfL7C-(x+J+9&7EsC~^O`&}V%)Ut8^O_7YAXPDpzv8ir4 zl`d)(;imc6r16k_d^)PJZ+QPxxVJS5e^4wX9D=V2zH&wW0-p&OJe=}rX`*->XT=;_qI&)=WHkYnZx6bLoUh_)n-A}SF_ z9z7agNTM5W6}}ui=&Qs@pO5$zHsOWIbd_&%j^Ok5PJ3yUWQw*i4*iKO)_er2CDUME ztt+{Egod~W-fn^aLe)aBz)MOc_?i-stTj}~iFk7u^-gGSbU;Iem06SDP=AEw9SzuF zeZ|hKCG3MV(z_PJg0(JbqTRf4T{NUt%kz&}4S`)0I%}ZrG!jgW2GwP=WTtkWS?DOs znI9LY!dK+1_H0h+i-_~URb^M;4&AMrEO_UlDV8o?E>^3x%ZJyh$JuDMrtYL8|G3If zPf2_Qb_W+V?$#O; zydKFv*%O;Y@o_T_UAYuaqx1isMKZ^32JtgeceA$0Z@Ck0;lHbS%N5)zzAW9iz; z8tTKeK7&qw!8XVz-+pz>z-BeIzr*#r0nB^cntjQ9@Y-N0=e&ZK72vlzX>f3RT@i7@ z=z`m7jNk!9%^xD0ug%ptZnM>F;Qu$rlwo}vRGBIymPL)L|x}nan3uFUw(&N z24gdkcb7!Q56{0<+zu zEtc5WzG2xf%1<@vo$ZsuOK{v9gx^0`gw>@h>ZMLy*h+6ueoie{D#}}` zK2@6Xxq(uZaLFC%M!2}FX}ab%GQ8A0QJ?&!vaI8Gv=vMhd);6kGguDmtuOElru()) zuRk&Z{?Vp!G~F<1#s&6io1`poBqpRHyM^p;7!+L??_DzJ8s9mYFMQ0^%_3ft7g{PD zZd}8E4EV}D!>F?bzcX=2hHR_P`Xy6?FOK)mCj)Ym4s2hh z0OlOdQa@I;^-3bhB6mpw*X5=0kJv8?#XP~9){G-+0ST@1Roz1qi8PhIXp1D$XNqVG zMl>WxwT+K`SdO1RCt4FWTNy3!i?N>*-lbnn#OxFJrswgD7HjuKpWh*o@QvgF&j+CT z{55~ZsUeR1aB}lv#s_7~+9dCix!5(KR#c?K?e2B%P$fvrsZxy@GP#R#jwL{y#Ld$} z7sF>QT6m|}?V;msb?Nlohj7a5W_D$y+4O6eI;Zt$jVGymlzLKscqer9#+p2$0It&u zWY!dCeM6^B^Z;ddEmhi?8`scl=Lhi7W%2|pT6X6^%-=q90DS(hQ-%c+E*ywPvmoF(KqDoW4!*gmQIklm zk#!GLqv|cs(JRF3G?=AYY19{w@~`G3pa z@xR9S-Hquh*&5Yas*VI};(%9%PADn`kzm zeWMJVW=>>wap*9|R7n#!&&J>gq04>DTCMtj{P^d12|2wXTEKvSf?$AvnE!peqV7i4 zE>0G%CSn%WCW1yre?yi9*aFP{GvZ|R4JT}M%x_%Hztz2qw?&28l&qW<6?c6ym{f$d z5YCF+k#yEbjCN|AGi~-NcCG8MCF1!MXBFL{#7q z)HO+WW173?kuI}^Xat;Q^gb4Hi0RGyB}%|~j8>`6X4CPo+|okMbKy9PHkr58V4bX6<&ERU)QlF8%%huUz&f+dwTN|tk+C&&o@Q1RtG`}6&6;ncQuAcfHoxd5AgD7`s zXynq41Y`zRSiOY@*;&1%1z>oNcWTV|)sjLg1X8ijg1Y zbIGL0X*Sd}EXSQ2BXCKbJmlckY(@EWn~Ut2lYeuw1wg?hhj@K?XB@V_ZP`fyL~Yd3n3SyHU-RwMBr6t-QWE5TinN9VD4XVPU; zonIIR!&pGqrLQK)=#kj40Im%V@ij0&Dh0*s!lnTw+D`Dt-xmk-jmpJv$1-E-vfYL4 zqKr#}Gm}~GPE+&$PI@4ag@=M}NYi7Y&HW82Q`@Y=W&PE31D110@yy(1vddLt`P%N^ z>Yz195A%tnt~tvsSR2{m!~7HUc@x<&`lGX1nYeQUE(%sphTi>JsVqSw8xql*Ys@9B z>RIOH*rFi*C`ohwXjyeRBDt8p)-u{O+KWP;$4gg||%*u{$~yEj+Al zE(hAQRQ1k7MkCq9s4^N3ep*$h^L%2Vq?f?{+cicpS8lo)$Cb69b98au+m2J_e7nYwID0@`M9XIo1H~|eZFc8Hl!qly612ADCVpU zY8^*RTMX(CgehD{9v|^9vZ6Rab`VeZ2m*gOR)Mw~73QEBiktViBhR!_&3l$|be|d6 zupC`{g89Y|V3uxl2!6CM(RNpdtynaiJ~*DqSTq9Mh`ohZnb%^3G{k;6%n18$4nAqR zjPOrP#-^Y9;iw{J@XH9=g5J+yEVh|e=4UeY<^65`%gWtdQ=-aqSgtywM(1nKXh`R4 zzPP&7r)kv_uC7X9n=h=!Zrf<>X=B5f<9~Q>h#jYRD#CT7D~@6@RGNyO-#0iq0uHV1 zPJr2O4d_xLmg2^TmG7|dpfJ?GGa`0|YE+`2Rata9!?$j#e9KfGYuLL(*^z z!SxFA`$qm)q-YKh)WRJZ@S+-sD_1E$V?;(?^+F3tVcK6 z2fE=8hV*2mgiAbefU^uvcM?&+Y&E}vG=Iz!%jBF7iv){lyC`)*yyS~D8k+Mx|N3bm zI~L~Z$=W9&`x)JnO;8c>3LSDw!fzN#X3qi|0`sXY4?cz{*#xz!kvZ9bO=K3XbN z5KrgN=&(JbXH{Wsu9EdmQ-W`i!JWEmfI;yVTT^a-8Ch#D8xf2dtyi?7p z%#)W3n*a#ndFpd{qN|+9Jz++AJQO#-Y7Z6%*%oyEP5zs}d&kKIr`FVEY z;S}@d?UU=tCdw~EJ{b}=9x}S2iv!!8<$?d7VKDA8h{oeD#S-$DV)-vPdGY@x08n)@ zag?yLF_E#evvRTj4^CcrLvBL=fft&@HOhZ6Ng4`8ijt&h2y}fOTC~7GfJi4vpomA5 zOcOM)o_I9BKz}I`q)fu+Qnfy*W`|mY%LO>eF^a z;$)?T4F-(X#Q-m}!-k8L_rNPf`Mr<9IWu)f&dvt=EL+ESYmCvErd@8B9hd)afc(ZL94S z?rp#h&{7Ah5IJftK4VjATklo7@hm?8BX*~oBiz)jyc9FuRw!-V;Uo>p!CWpLaIQyt zAs5WN)1CCeux-qiGdmbIk8LR`gM+Qg=&Ve}w?zA6+sTL)abU=-cvU`3E?p5$Hpkxw znu0N659qR=IKnde*AEz_7z2pdi_Bh-sb3b=PdGO1Pdf_q2;+*Cx9YN7p_>rl``knY zRn%aVkcv1(W;`Mtp_DNOIECtgq%ufk-mu_<+Fu3Q17Tq4Rr(oeq)Yqk_CHA7LR@7@ zIZIDxxhS&=F2IQfusQ+Nsr%*zFK7S4g!U0y@3H^Yln|i;0a5+?RPG;ZSp6Tul>ezM z`40+516&719qT)mW|ArDSENle5hE2e8qY+zfeZoy12u&xoMgcP)4=&P-1Ib*-bAy` zlT?>w&B|ei-rCXO;sxo7*G;!)_p#%PAM-?m$JP(R%x1Hfas@KeaG%LO?R=lmkXc_MKZW}3f%KZ*rAN?HYvbu2L$ zRt_uv7~-IejlD1x;_AhwGXjB94Q=%+PbxuYzta*jw?S&%|qb=(JfJ?&6P=R7X zV%HP_!@-zO*zS}46g=J}#AMJ}rtWBr21e6hOn&tEmaM%hALH7nlm2@LP4rZ>2 zebe5aH@k!e?ij4Zwak#30|}>;`bquDQK*xmR=zc6vj0yuyC6+U=LusGnO3ZKFRpen z#pwzh!<+WBVp-!$MAc<0i~I%fW=8IO6K}bJ<-Scq>e+)951R~HKB?Mx2H}pxPHE@} zvqpq5j81_jtb_WneAvp<5kgdPKm|u2BdQx9%EzcCN&U{l+kbkhmV<1}yCTDv%&K^> zg;KCjwh*R1f_`6`si$h6`jyIKT7rTv5#k~x$mUyIw)_>Vr)D4fwIs@}{FSX|5GB1l z4vv;@oS@>Bu7~{KgUa_8eg#Lk6IDT2IY$41$*06{>>V;Bwa(-@N;ex4;D`(QK*b}{ z{#4$Hmt)FLqERgKz=3zXiV<{YX6V)lvYBr3V>N6ajeI~~hGR5Oe>W9r@sg)Na(a4- zxm%|1OKPN6^%JaD^^O~HbLSu=f`1px>RawOxLr+1b2^28U*2#h*W^=lSpSY4(@*^l z{!@9RSLG8Me&RJYLi|?$c!B0fP=4xAM4rerxX{xy{&i6=AqXueQAIBqO+pmuxy8Ib z4X^}r!NN3-upC6B#lt7&x0J;)nb9O~xjJMemm$_fHuP{DgtlU3xiW0UesTzS30L+U zQzDI3p&3dpONhd5I8-fGk^}@unluzu%nJ$9pzoO~Kk!>dLxw@M)M9?pNH1CQhvA`z zV;uacUtnBTdvT`M$1cm9`JrT3BMW!MNVBy%?@ZX%;(%(vqQAz<7I!hlDe|J3cn9=} zF7B;V4xE{Ss76s$W~%*$JviK?w8^vqCp#_G^jN0j>~Xq#Zru26e#l3H^{GCLEXI#n z?n~F-Lv#hU(bZS`EI9(xGV*jT=8R?CaK)t8oHc9XJ;UPY0Hz$XWt#QyLBaaz5+}xM zXk(!L_*PTt7gwWH*HLWC$h3Ho!SQ-(I||nn_iEC{WT3S{3V{8IN6tZ1C+DiFM{xlI zeMMk{o5;I6UvaC)@WKp9D+o?2Vd@4)Ue-nYci()hCCsKR`VD;hr9=vA!cgGL%3k^b(jADGyPi2TKr(JNh8mzlIR>n(F_hgiV(3@Ds(tjbNM7GoZ;T|3 zWzs8S`5PrA!9){jBJuX4y`f<4;>9*&NY=2Sq2Bp`M2(fox7ZhIDe!BaQUb@P(ub9D zlP8!p(AN&CwW!V&>H?yPFMJ)d5x#HKfwx;nS{Rr@oHqpktOg)%F+%1#tsPtq7zI$r zBo-Kflhq-=7_eW9B2OQv=@?|y0CKN77)N;z@tcg;heyW{wlpJ1t`Ap!O0`Xz{YHqO zI1${8Hag^r!kA<2_~bYtM=<1YzQ#GGP+q?3T7zYbIjN6Ee^V^b&9en$8FI*NIFg9G zPG$OXjT0Ku?%L7fat8Mqbl1`azf1ltmKTa(HH$Dqlav|rU{zP;Tbnk-XkGFQ6d+gi z-PXh?_kEJl+K98&OrmzgPIijB4!Pozbxd0H1;Usy!;V>Yn6&pu*zW8aYx`SC!$*ti zSn+G9p=~w6V(fZZHc>m|PPfjK6IN4(o=IFu?pC?+`UZAUTw!e`052{P=8vqT^(VeG z=psASIhCv28Y(;7;TuYAe>}BPk5Qg=8$?wZj9lj>h2kwEfF_CpK=+O6Rq9pLn4W)# zeXCKCpi~jsfqw7Taa0;!B5_C;B}e56W1s8@p*)SPzA;Fd$Slsn^=!_&!mRHV*Lmt| zBGIDPuR>CgS4%cQ4wKdEyO&Z>2aHmja;Pz+n|7(#l%^2ZLCix%>@_mbnyPEbyrHaz z>j^4SIv;ZXF-Ftzz>*t4wyq)ng8%0d;(Z_ExZ-cxwei=8{(br-`JYO(f23Wae_MqE z3@{Mlf^%M5G1SIN&en1*| zH~ANY1h3&WNsBy$G9{T=`kcxI#-X|>zLX2r*^-FUF+m0{k)n#GTG_mhG&fJfLj~K& zU~~6othMlvMm9<*SUD2?RD+R17|Z4mgR$L*R3;nBbo&Vm@39&3xIg;^aSxHS>}gwR zmzs?h8oPnNVgET&dx5^7APYx6Vv6eou07Zveyd+^V6_LzI$>ic+pxD_8s~ zC<}ucul>UH<@$KM zT4oI=62M%7qQO{}re-jTFqo9Z;rJKD5!X5$iwUsh*+kcHVhID08MB5cQD4TBWB(rI zuWc%CA}}v|iH=9gQ?D$1#Gu!y3o~p7416n54&Hif`U-cV?VrUMJyEqo_NC4#{puzU zzXEE@UppeeRlS9W*^N$zS`SBBi<@tT+<%3l@KhOy^%MWB9(A#*J~DQ;+MK*$rxo6f zcx3$3mcx{tly!q(p2DQrxcih|)0do_ZY77pyHGE#Q(0k*t!HUmmMcYFq%l$-o6%lS zDb49W-E?rQ#Hl``C3YTEdGZjFi3R<>t)+NAda(r~f1cT5jY}s7-2^&Kvo&2DLTPYP zhVVo-HLwo*vl83mtQ9)PR#VBg)FN}+*8c-p8j`LnNUU*Olm1O1Qqe62D#$CF#?HrM zy(zkX|1oF}Z=T#3XMLWDrm(|m+{1&BMxHY7X@hM_+cV$5-t!8HT(dJi6m9{ja53Yw z3f^`yb6Q;(e|#JQIz~B*=!-GbQ4nNL-NL z@^NWF_#w-Cox@h62;r^;Y`NX8cs?l^LU;5IWE~yvU8TqIHij!X8ydbLlT0gwmzS9} z@5BccG?vO;rvCs$mse1*ANi-cYE6Iauz$Fbn3#|ToAt5v7IlYnt6RMQEYLldva{~s zvr>1L##zmeoYgvIXJ#>bbuCVuEv2ZvZ8I~PQUN3wjP0UC)!U+wn|&`V*8?)` zMSCuvnuGec>QL+i1nCPGDAm@XSMIo?A9~C?g2&G8aNKjWd2pDX{qZ?04+2 zeyLw}iEd4vkCAWwa$ zbrHlEf3hfN7^1g~aW^XwldSmx1v~1z(s=1az4-wl} z`mM+G95*N*&1EP#u3}*KwNrPIgw8Kpp((rdEOO;bT1;6ea~>>sK+?!;{hpJ3rR<6UJb`O8P4@{XGgV%63_fs%cG8L zk9Fszbdo4tS$g0IWP1>t@0)E%-&9yj%Q!fiL2vcuL;90fPm}M==<>}Q)&sp@STFCY z^p!RzmN+uXGdtPJj1Y-khNyCb6Y$Vs>eZyW zPaOV=HY_T@FwAlleZCFYl@5X<<7%5DoO(7S%Lbl55?{2vIr_;SXBCbPZ(up;pC6Wx={AZL?shYOuFxLx1*>62;2rP}g`UT5+BHg(ju z&7n5QSvSyXbioB9CJTB#x;pexicV|9oaOpiJ9VK6EvKhl4^Vsa(p6cIi$*Zr0UxQ z;$MPOZnNae2Duuce~7|2MCfhNg*hZ9{+8H3?ts9C8#xGaM&sN;2lriYkn9W>&Gry! z3b(Xx1x*FhQkD-~V+s~KBfr4M_#0{`=Yrh90yj}Ph~)Nx;1Y^8<418tu!$1<3?T*~ z7Dl0P3Uok-7w0MPFQexNG1P5;y~E8zEvE49>$(f|XWtkW2Mj`udPn)pb%} zrA%wRFp*xvDgC767w!9`0vx1=q!)w!G+9(-w&p*a@WXg{?T&%;qaVcHo>7ca%KX$B z^7|KBPo<2;kM{2mRnF8vKm`9qGV%|I{y!pKm8B(q^2V;;x2r!1VJ^Zz8bWa)!-7a8 zSRf@dqEPlsj!7}oNvFFAA)75})vTJUwQ03hD$I*j6_5xbtd_JkE2`IJD_fQ;a$EkO z{fQ{~e%PKgPJsD&PyEvDmg+Qf&p*-qu!#;1k2r_(H72{^(Z)htgh@F?VIgK#_&eS- z$~(qInec>)XIkv@+{o6^DJLpAb>!d}l1DK^(l%#OdD9tKK6#|_R?-%0V!`<9Hj z3w3chDwG*SFte@>Iqwq`J4M&{aHXzyigT620+Vf$X?3RFfeTcvx_e+(&Q*z)t>c0e zpZH$1Z3X%{^_vylHVOWT6tno=l&$3 z9^eQ@TwU#%WMQaFvaYp_we%_2-9=o{+ck zF{cKJCOjpW&qKQquyp2BXCAP920dcrZ}T1@piukx_NY;%2W>@Wca%=Ch~x5Oj58Hv z;D-_ALOZBF(Mqbcqjd}P3iDbek#Dwzu`WRs`;hRIr*n0PV7vT+%Io(t}8KZ zpp?uc2eW!v28ipep0XNDPZt7H2HJ6oey|J3z!ng#1H~x_k%35P+Cp%mqXJ~cV0xdd z^4m5^K_dQ^Sg?$P`))ccV=O>C{Ds(C2WxX$LMC5vy=*44pP&)X5DOPYfqE${)hDg< z3hcG%U%HZ39=`#Ko4Uctg&@PQLf>?0^D|4J(_1*TFMOMB!Vv1_mnOq$BzXQdOGqgy zOp#LBZ!c>bPjY1NTXksZmbAl0A^Y&(%a3W-k>bE&>K?px5Cm%AT2E<&)Y?O*?d80d zgI5l~&Mve;iXm88Q+Fw7{+`PtN4G7~mJWR^z7XmYQ>uoiV!{tL)hp|= zS(M)813PM`d<501>{NqaPo6BZ^T{KBaqEVH(2^Vjeq zgeMeMpd*1tE@@);hGjuoVzF>Cj;5dNNwh40CnU+0DSKb~GEMb_# zT8Z&gz%SkHq6!;_6dQFYE`+b`v4NT7&@P>cA1Z1xmXy<2htaDhm@XXMp!g($ zw(7iFoH2}WR`UjqjaqOQ$ecNt@c|K1H1kyBArTTjLp%-M`4nzOhkfE#}dOpcd;b#suq8cPJ&bf5`6Tq>ND(l zib{VrPZ>{KuaIg}Y$W>A+nrvMg+l4)-@2jpAQ5h(Tii%Ni^-UPVg{<1KGU2EIUNGaXcEkOedJOusFT9X3%Pz$R+-+W+LlRaY-a$5r?4V zbPzgQl22IPG+N*iBRDH%l{Zh$fv9$RN1sU@Hp3m=M}{rX%y#;4(x1KR2yCO7Pzo>rw(67E{^{yUR`91nX^&MxY@FwmJJbyPAoWZ9Z zcBS$r)&ogYBn{DOtD~tIVJUiq|1foX^*F~O4hlLp-g;Y2wKLLM=?(r3GDqsPmUo*? zwKMEi*%f)C_@?(&&hk>;m07F$X7&i?DEK|jdRK=CaaNu-)pX>n3}@%byPKVkpLzBq z{+Py&!`MZ^4@-;iY`I4#6G@aWMv{^2VTH7|WF^u?3vsB|jU3LgdX$}=v7#EHRN(im zI(3q-eU$s~r=S#EWqa_2!G?b~ z<&brq1vvUTJH380=gcNntZw%7UT8tLAr-W49;9y^=>TDaTC|cKA<(gah#2M|l~j)w zY8goo28gj$n&zcNgqX1Qn6=<8?R0`FVO)g4&QtJAbW3G#D)uNeac-7cH5W#6i!%BH z=}9}-f+FrtEkkrQ?nkoMQ1o-9_b+&=&C2^h!&mWFga#MCrm85hW;)1pDt;-uvQG^D zntSB?XA*0%TIhtWDS!KcI}kp3LT>!(Nlc(lQN?k^bS8Q^GGMfo}^|%7s;#r+pybl@?KA++|FJ zr%se9(B|g*ERQU96az%@4gYrxRRxaM2*b}jNsG|0dQi;Rw{0WM0E>rko!{QYAJJKY z)|sX0N$!8d9E|kND~v|f>3YE|uiAnqbkMn)hu$if4kUkzKqoNoh8v|S>VY1EKmgO} zR$0UU2o)4i4yc1inx3}brso+sio{)gfbLaEgLahj8(_Z#4R-v) zglqwI%`dsY+589a8$Mu7#7_%kN*ekHupQ#48DIN^uhDxblDg3R1yXMr^NmkR z7J_NWCY~fhg}h!_aXJ#?wsZF$q`JH>JWQ9`jbZzOBpS`}-A$Vgkq7+|=lPx9H7QZG z8i8guMN+yc4*H*ANr$Q-3I{FQ-^;8ezWS2b8rERp9TMOLBxiG9J*g5=?h)mIm3#CGi4JSq1ohFrcrxx@`**K5%T}qbaCGldV!t zVeM)!U3vbf5FOy;(h08JnhSGxm)8Kqxr9PsMeWi=b8b|m_&^@#A3lL;bVKTBx+0v8 zLZeWAxJ~N27lsOT2b|qyp$(CqzqgW@tyy?CgwOe~^i;ZH zlL``i4r!>i#EGBNxV_P@KpYFQLz4Bdq{#zA&sc)*@7Mxsh9u%e6Ke`?5Yz1jkTdND zR8!u_yw_$weBOU}24(&^Bm|(dSJ(v(cBct}87a^X(v>nVLIr%%D8r|&)mi+iBc;B;x;rKq zd8*X`r?SZsTNCPQqoFOrUz8nZO?225Z#z(B!4mEp#ZJBzwd7jW1!`sg*?hPMJ$o`T zR?KrN6OZA1H{9pA;p0cSSu;@6->8aJm1rrO-yDJ7)lxuk#npUk7WNER1Wwnpy%u zF=t6iHzWU(L&=vVSSc^&D_eYP3TM?HN!Tgq$SYC;pSIPWW;zeNm7Pgub#yZ@7WPw#f#Kl)W4%B>)+8%gpfoH1qZ;kZ*RqfXYeGXJ_ zk>2otbp+1By`x^1V!>6k5v8NAK@T;89$`hE0{Pc@Q$KhG0jOoKk--Qx!vS~lAiypV zCIJ&6B@24`!TxhJ4_QS*S5;;Pk#!f(qIR7*(c3dN*POKtQe)QvR{O2@QsM%ujEAWEm) z+PM=G9hSR>gQ`Bv2(k}RAv2+$7qq(mU`fQ+&}*i%-RtSUAha>70?G!>?w%F(b4k!$ zvm;E!)2`I?etmSUFW7WflJ@8Nx`m_vE2HF#)_BiD#FaNT|IY@!uUbd4v$wTglIbIX zblRy5=wp)VQzsn0_;KdM%g<8@>#;E?vypTf=F?3f@SSdZ;XpX~J@l1;p#}_veWHp>@Iq_T z@^7|h;EivPYv1&u0~l9(a~>dV9Uw10QqB6Dzu1G~-l{*7IktljpK<_L8m0|7VV_!S zRiE{u97(%R-<8oYJ{molUd>vlGaE-C|^<`hppdDz<7OS13$#J zZ+)(*rZIDSt^Q$}CRk0?pqT5PN5TT`Ya{q(BUg#&nAsg6apPMhLTno!SRq1e60fl6GvpnwDD4N> z9B=RrufY8+g3_`@PRg+(+gs2(bd;5#{uTZk96CWz#{=&h9+!{_m60xJxC%r&gd_N! z>h5UzVX%_7@CUeAA1XFg_AF%(uS&^1WD*VPS^jcC!M2v@RHZML;e(H-=(4(3O&bX- zI6>usJOS+?W&^S&DL{l|>51ZvCXUKlH2XKJPXnHjs*oMkNM#ZDLx!oaM5(%^)5XaP zk6&+P16sA>vyFe9v`Cp5qnbE#r#ltR5E+O3!WnKn`56Grs2;sqr3r# zp@Zp<^q`5iq8OqOlJ`pIuyK@3zPz&iJ0Jcc`hDQ1bqos2;}O|$i#}e@ua*x5VCSx zJAp}+?Hz++tm9dh3Fvm_bO6mQo38al#>^O0g)Lh^&l82+&x)*<n7^Sw-AJo9tEzZDwyJ7L^i7|BGqHu+ea6(&7jKpBq>~V z8CJxurD)WZ{5D0?s|KMi=e7A^JVNM6sdwg@1Eg_+Bw=9j&=+KO1PG|y(mP1@5~x>d z=@c{EWU_jTSjiJl)d(>`qEJ;@iOBm}alq8;OK;p(1AdH$)I9qHNmxxUArdzBW0t+Qeyl)m3?D09770g z)hzXEOy>2_{?o%2B%k%z4d23!pZcoxyW1Ik{|m7Q1>fm4`wsRrl)~h z_=Z*zYL+EG@DV1{6@5@(Ndu!Q$l_6Qlfoz@79q)Kmsf~J7t1)tl#`MD<;1&CAA zH8;i+oBm89dTTDl{aH`cmTPTt@^K-%*sV+t4X9q0Z{A~vEEa!&rRRr=0Rbz4NFCJr zLg2u=0QK@w9XGE=6(-JgeP}G#WG|R&tfHRA3a9*zh5wNTBAD;@YYGx%#E4{C#Wlfo z%-JuW9=FA_T6mR2-Vugk1uGZvJbFvVVWT@QOWz$;?u6+CbyQsbK$>O1APk|xgnh_8 zc)s@Mw7#0^wP6qTtyNq2G#s?5j~REyoU6^lT7dpX{T-rhZWHD%dik*=EA7bIJgOVf_Ga!yC8V^tkTOEHe+JK@Fh|$kfNxO^= z#lpV^(ZQ-3!^_BhV>aXY~GC9{8%1lOJ}6vzXDvPhC>JrtXwFBC+!3a*Z-%#9}i z#<5&0LLIa{q!rEIFSFc9)>{-_2^qbOg5;_A9 ztQ))C6#hxSA{f9R3Eh^`_f${pBJNe~pIQ`tZVR^wyp}=gLK}e5_vG@w+-mp#Fu>e| z*?qBp5CQ5zu+Fi}xAs)YY1;bKG!htqR~)DB$ILN6GaChoiy%Bq@i+1ZnANC0U&D z_4k$=YP47ng+0NhuEt}6C;9-JDd8i5S>`Ml==9wHDQFOsAlmtrVwurYDw_)Ihfk35 zJDBbe!*LUpg%4n>BExWz>KIQ9vexUu^d!7rc_kg#Bf= z7TLz|l*y*3d2vi@c|pX*@ybf!+Xk|2*z$@F4K#MT8Dt4zM_EcFmNp31#7qT6(@GG? zdd;sSY9HHuDb=w&|K%sm`bYX#%UHKY%R`3aLMO?{T#EI@FNNFNO>p@?W*i0z(g2dt z{=9Ofh80Oxv&)i35AQN>TPMjR^UID-T7H5A?GI{MD_VeXZ%;uo41dVm=uT&ne2h0i zv*xI%9vPtdEK@~1&V%p1sFc2AA`9?H)gPnRdlO~URx!fiSV)j?Tf5=5F>hnO=$d$x zzaIfr*wiIc!U1K*$JO@)gP4%xp!<*DvJSv7p}(uTLUb=MSb@7_yO+IsCj^`PsxEl& zIxsi}s3L?t+p+3FXYqujGhGwTx^WXgJ1}a@Yq5mwP0PvGEr*qu7@R$9j>@-q1rz5T zriz;B^(ex?=3Th6h;7U`8u2sDlfS{0YyydK=*>-(NOm9>S_{U|eg(J~C7O zIe{|LK=Y`hXiF_%jOM8Haw3UtaE{hWdzo3BbD6ud7br4cODBtN(~Hl+odP0SSWPw;I&^m)yLw+nd#}3#z}?UIcX3=SssI}`QwY=% zAEXTODk|MqTx}2DVG<|~(CxgLyi*A{m>M@1h^wiC)4Hy>1K7@|Z&_VPJsaQoS8=ex zDL&+AZdQa>ylxhT_Q$q=60D5&%pi6+qlY3$3c(~rsITX?>b;({FhU!7HOOhSP7>bmTkC8KM%!LRGI^~y3Ug+gh!QM=+NZXznM)?L3G=4=IMvFgX3BAlyJ z`~jjA;2z+65D$j5xbv9=IWQ^&-K3Yh`vC(1Qz2h2`o$>Cej@XRGff!it$n{@WEJ^N z41qk%Wm=}mA*iwCqU_6}Id!SQd13aFER3unXaJJXIsSnxvG2(hSCP{i&QH$tL&TPx zDYJsuk+%laN&OvKb-FHK$R4dy%M7hSB*yj#-nJy?S9tVoxAuDei{s}@+pNT!vLOIC z8g`-QQW8FKp3cPsX%{)0B+x+OhZ1=L7F-jizt|{+f1Ga7%+!BXqjCjH&x|3%?UbN# zh?$I1^YokvG$qFz5ySK+Ja5=mkR&p{F}ev**rWdKMko+Gj^?Or=UH?SCg#0F(&a_y zXOh}dPv0D9l0RVedq1~jCNV=8?vZfU-Xi|nkeE->;ohG3U7z+^0+HV17~-_Mv#mV` zzvwUJJ15v5wwKPv-)i@dsEo@#WEO9zie7mdRAbgL2kjbW4&lk$vxkbq=w5mGKZK6@ zjXWctDkCRx58NJD_Q7e}HX`SiV)TZMJ}~zY6P1(LWo`;yDynY_5_L?N-P`>ALfmyl z8C$a~FDkcwtzK9m$tof>(`Vu3#6r#+v8RGy#1D2)F;vnsiL&P-c^PO)^B-4VeJteLlT@25sPa z%W~q5>YMjj!mhN})p$47VA^v$Jo6_s{!y?}`+h+VM_SN`!11`|;C;B};B&Z<@%FOG z_YQVN+zFF|q5zKab&e4GH|B;sBbKimHt;K@tCH+S{7Ry~88`si7}S)1E{21nldiu5 z_4>;XTJa~Yd$m4A9{Qbd)KUAm7XNbZ4xHbg3a8-+1uf*$1PegabbmCzgC~1WB2F(W zYj5XhVos!X!QHuZXCatkRsdEsSCc+D2?*S7a+(v%toqyxhjz|`zdrUvsxQS{J>?c& zvx*rHw^8b|v^7wq8KWVofj&VUitbm*a&RU_ln#ZFA^3AKEf<#T%8I!Lg3XEsdH(A5 zlgh&M_XEoal)i#0tcq8c%Gs6`xu;vvP2u)D9p!&XNt z!TdF_H~;`g@fNXkO-*t<9~;iEv?)Nee%hVe!aW`N%$cFJ(Dy9+Xk*odyFj72T!(b%Vo5zvCGZ%3tkt$@Wcx8BWEkefI1-~C_3y*LjlQ5%WEz9WD8i^ z2MV$BHD$gdPJV4IaV)G9CIFwiV=ca0cfXdTdK7oRf@lgyPx;_7*RRFk=?@EOb9Gcz zg~VZrzo*Snp&EE{$CWr)JZW)Gr;{B2ka6B!&?aknM-FENcl%45#y?oq9QY z3^1Y5yn&^D67Da4lI}ljDcphaEZw2;tlYuzq?uB4b9Mt6!KTW&ptxd^vF;NbX=00T z@nE1lIBGgjqs?ES#P{ZfRb6f!At51vk%<0X%d_~NL5b8UyfQMPDtfU@>ijA0NP3UU zh{lCf`Wu7cX!go`kUG`1K=7NN@SRGjUKuo<^;@GS!%iDXbJs`o6e`v3O8-+7vRkFm z)nEa$sD#-v)*Jb>&Me+YIW3PsR1)h=-Su)))>-`aRcFJG-8icomO4J@60 zw10l}BYxi{eL+Uu0xJYk-Vc~BcR49Qyyq!7)PR27D`cqGrik=?k1Of>gY7q@&d&Ds zt7&WixP`9~jjHO`Cog~RA4Q%uMg+$z^Gt&vn+d3&>Ux{_c zm|bc;k|GKbhZLr-%p_f%dq$eiZ;n^NxoS-Nu*^Nx5vm46)*)=-Bf<;X#?`YC4tLK; z?;u?shFbXeks+dJ?^o$l#tg*1NA?(1iFff@I&j^<74S!o;SWR^Xi);DM%8XiWpLi0 zQE2dL9^a36|L5qC5+&Pf0%>l&qQ&)OU4vjd)%I6{|H+pw<0(a``9w(gKD&+o$8hOC zNAiShtc}e~ob2`gyVZx59y<6Fpl*$J41VJ-H*e-yECWaDMmPQi-N8XI3 z%iI@ljc+d}_okL1CGWffeaejlxWFVDWu%e=>H)XeZ|4{HlbgC-Uvof4ISYQzZ0Um> z#Ov{k1c*VoN^f(gfiueuag)`TbjL$XVq$)aCUBL_M`5>0>6Ska^*Knk__pw{0I>jA zzh}Kzg{@PNi)fcAk7jMAdi-_RO%x#LQszDMS@_>iFoB+zJ0Q#CQJzFGa8;pHFdi`^ zxnTC`G$7Rctm3G8t8!SY`GwFi4gF|+dAk7rh^rA{NXzc%39+xSYM~($L(pJ(8Zjs* zYdN_R^%~LiGHm9|ElV4kVZGA*T$o@YY4qpJOxGHlUi*S*A(MrgQ{&xoZQo+#PuYRs zv3a$*qoe9gBqbN|y|eaH=w^LE{>kpL!;$wRahY(hhzRY;d33W)m*dfem@)>pR54Qy z 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 0f8d593..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem 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" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -: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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index cf58543..0000000 --- a/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user manual at https://docs.gradle.org/5.2.1/userguide/multi_project_builds.html - */ - -rootProject.name = 'JobServ' diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index e69de29..cbc911a 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -0,0 +1,216 @@ +package JobServ; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.StatusRuntimeException; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.Scanner; + +// GRPC Client Class +public class JobServClient { + + // RequestMessage types + // TODO: refactor to enum? + public static final int OUTPUT = 0; + public static final int RETURN = 1; + public static final int PAUSE = 2; + public static final int RESUME = 3; + public static final int STATUS = 4; + + /* The client should not use the same logging module as the server. + * In a more robust product the server logging module will take advantage of system level + * log aggregators such as journalctl, which the client should not be writing to on the users system + */ + private static final Logger logger = Logger.getLogger(JobServClient.class.getName()); + + private final ManagedChannel channel; + + /* blockingStub is used when the client needs to block until the server responds + * the client doesnt nessesarily need to support asynchronously firing off commands + * in this shell-like interface it would be disconcerting to get multiple returns out of order + */ + private final ShellServerGrpc.ShellServerBlockingStub blockingStub; + + // Constructor connects to server + public JobServClient(String host, int port) { + this(ManagedChannelBuilder.forAddress(host, port) + // TODO: MTLS + .usePlaintext() + .build()); + } + + // private overload of constructor, used in the above constructor + JobServClient(ManagedChannel channel) { + this.channel = channel; + blockingStub = ShellServerGrpc.newBlockingStub(channel); + } + + public void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + // sends the server a RequestMessage, blocks until response + // returns an integer representing process status + private int makeProcessRequest(int pid, int requestType) { + logger.info("[+] Sending request"); + + RequestMessage request = RequestMessage.newBuilder() + .setPID(pid) + .setRequestType(requestType) + .build(); + JobStatusMessage response; + + try { + // blocking network operation + response = blockingStub.getStatusDetail(request); + } catch (StatusRuntimeException e) { + /* if this was using an async stub it might be + * worthwhile to include the PID and request type + */ + logger.log(Level.WARNING, "[-]Status Request Failed: %s", e.getStatus()); + return -1; + } + + // return process status + return response.getProcessStatus(); + } + + // sends the server a request for output from PID + // different from getProcessStatus in output expected from the server + // returns process output as string + public String getProcessOutput(int pid) { + logger.info("[+] requesting output"); + + RequestMessage request = RequestMessage.newBuilder() + .setPID(pid) + .setRequestType(OUTPUT) + .build(); + OutputMessage response; + + try { + // blocking network operation + response = blockingStub.getJobOutput(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "[-] Request for output failed: %s", e.getStatus()); + // TODO: refactor this out by throwing here and catching in the shell + return ""; + } + + return response.getProcessOutput(); + } + + // sends the server a command for a new job, blocks until response + // returns new pid of job + public int sendNewJobMessage(String command) { + // thought of escaping this, but the vulnerability is only client side, from client user input. + logger.info("[+] Sending command to server"); + + NewJobMessage request = NewJobMessage.newBuilder() + .setCommand(command) + .build(); + JobStatusMessage response; + + try { + // blocking network operation + response = blockingStub.makeNewJob(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "[-] Request for new job failed!"); + return -1; + } + + if(response.getPID() == -1) { + logger.log(Level.WARNING, "New job creation failed server side!"); + } + + return response.getPID(); + } + + // Client entrypoint + public static void main(String[] args) throws Exception { + // check args + if(args.length != 3) { + System.out.println("Usage: $ jobservclient host port"); + return; + } + + // start client (or fail if port is improperly formatted) + JobServClient client; + try { + client = new JobServClient(args[1], Integer.parseInt(args[2])); + } catch (NumberFormatException e) { + System.out.println("Invalid Port"); + return; + } + + Scanner reader = new Scanner(System.in); + while(true) { + System.out.print("> "); + int pid, status; + String input = reader.next(); + switch (input) { + + case "pause": + System.out.println("Enter a PID"); + pid = reader.nextInt(); + status = client.makeProcessRequest(pid, PAUSE); + // TODO: parse status return + break; + + case "resume": + System.out.println("Enter a PID"); + pid = reader.nextInt(); + status = client.makeProcessRequest(pid, RESUME); + // TODO: parse status return + break; + + case "new": + System.out.println("Enter a command"); + String command = reader.next(); + pid = client.sendNewJobMessage(command); + System.out.println(String.format("New process on server: %d", pid)); + break; + + case "output": + System.out.println("Enter a PID"); + pid = reader.nextInt(); + String out = client.getProcessOutput(pid); + System.out.println(out); + break; + + case "status": + System.out.println("Enter a PID"); + pid = reader.nextInt(); + status = client.makeProcessRequest(pid, STATUS); + System.out.println(String.format("Current status of program is: %d", status)); + break; + + case "return": + System.out.println("Enter a PID"); + pid = reader.nextInt(); + status = client.makeProcessRequest(pid, RETURN); + System.out.println(String.format("Exit code of process is: %d", status)); + break; + + case "quit": + reader.close(); + return; + + case "help": + System.out.println("pause: pauses a process on the server"); + System.out.println("resume: resumes a process on the server"); + System.out.println("new: starts a new process on the server"); + System.out.println("output: garners (new) output from a process on the server"); + System.out.println("status: outputs the current status of a program on the server"); + System.out.println("return: outputs exit code of a process on the sercer"); + break; + + default: + System.out.println("Improper output, try 'help'"); + break; + } + } + } +} diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 49bd771..135760d 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -14,81 +14,80 @@ public class JobServServer { // starts the GRPC API Server private void start() throws IOException { - // TODO: this should be passed in from a configuration manager - int port = 8448; - server = ServerBUilder.forPort(port) - .addService(new JobServImpl()) - .build() - .start(); - logger.info("Server initialized without tls"); - Runtime.getRuntime().addShutdownHook(new thread() { - @Override - public void run() { - // JVM shutdown might break logger functionality - // so investigate this.... - logger.info("Shutting down server"); - JobServServer.this.stop(); - } - }); + // TODO: this should be passed in from a configuration manager + int port = 8448; + server = ServerBUilder.forPort(port) + .addService(new JobServImpl()) + .build() + .start(); + logger.info("Server initialized without tls"); + Runtime.getRuntime().addShutdownHook(new thread() { + @Override + public void run() { + // JVM shutdown might break logger functionality + // so investigate this.... + logger.info("Shutting down server"); + JobServServer.this.stop(); + } + }); } private void stop() { - if (server != null) { - server.shutdown(); - } + if (server != null) { + server.shutdown(); + } } - + // spinlock for main() private void blockUntilShutdown() throws InterruptedException { - if (server != null) { - server.awaitTermination(); - } + if (server != null) { + server.awaitTermination(); + } } - // Main funciton. starts GRPC server and spins until server is shutdown + // Main function. starts GRPC server and spins until server is shutdown public static void main(String[] args) throws IOException, InterruptedException { - final JobServServer server = new JobServServer(); - server.start(); - server.blockUntilShutdown(); + final JobServServer server = new JobServServer(); + server.start(); + server.blockUntilShutdown(); } // wrap around stub code generated by GRPC - static class JobServImpl extends JobServGrpc.JobServImplBase { + static class JobServImpl extends JobServ.JobServImplBase { - @Override - public void getStatusDetail(RequestMessage request, - StreamObserver responder) { - // TODO: Implement job controller, check request type, do the thing - JobStatusMessage reply = JobStatusMessage.newBuilder() - .setPID(-1) - .setProcessStatus(-1) - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void getJobOutput(RequestMessage request, - StreamObserver responder) { - // TODO: Implement job controller, get output, do the thing - OutputMessage reply = OutputMessage.newBuilder() - .setPID(-1) - .setProcessOutput("Hello World!") - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void makeNewJob(NewJobMessage request, - StreamObserver responder) { - // TODO: Implement job controller, Start Job, do the thing - JobStatusMessage reply = JobStatusMessage.newBuilder() - .setPID(-1) - .setProcessStatus(-1) - .build(): - responder.onNext(reply); - responder.onCompleted(); - } + @Override + public void getStatusDetail(RequestMessage request, + StreamObserver responder) { + // TODO: Implement job controller, check request type, do the thing + JobStatusMessage reply = JobStatusMessage.newBuilder() + .setPID(-1) + .setProcessStatus(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void getJobOutput(RequestMessage request, + StreamObserver responder) { + // TODO: Implement job controller, get output, do the thing + OutputMessage reply = OutputMessage.newBuilder() + .setPID(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void makeNewJob(NewJobMessage request, + StreamObserver responder) { + // TODO: Implement job controller, Start Job, do the thing + JobStatusMessage reply = JobStatusMessage.newBuilder() + .setPID(-1) + .setJobStatus(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } } } diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index 2256f87..5f71bca 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -1,13 +1,13 @@ syntax = "proto3"; option java_multiple_files = true; -option java_package = "com.ajhahn.jobserv"; -option java_outer_classname = "JobServProto"; +option java_package = "JobServ"; +option java_outer_classname = "JobServGrpc"; option objc_class_prefix = "JSV"; -package jobserv; +package JobServ; -service jobserv { +service ShellServer { rpc getStatusDetail (RequestMessage) returns (JobStatusMessage) {} rpc getJobOutput (RequestMessage) returns (OutputMessage) {} rpc makeNewJob (NewJobMessage) returns (JobStatusMessage) {} @@ -28,6 +28,5 @@ message JobStatusMessage { } message OutputMessage { - int32 PID = 1; string ProcessOutput = 2; } diff --git a/src/test/java/JobServ/AppTest.java b/src/test/java/JobServ/AppTest.java deleted file mode 100644 index 6a7ef0a..0000000 --- a/src/test/java/JobServ/AppTest.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -package JobServ; - -import org.junit.Test; -import static org.junit.Assert.*; - -public class AppTest { - @Test public void testAppHasAGreeting() { - App classUnderTest = new App(); - assertNotNull("app should have a greeting", classUnderTest.getGreeting()); - } -} From e476438c8c6bc50f3d19067f6b816309ac240743 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 01:35:55 -0700 Subject: [PATCH 06/63] Server builds --- src/main/java/JobServ/JobServServer.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 135760d..985db88 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -16,12 +16,12 @@ public class JobServServer { private void start() throws IOException { // TODO: this should be passed in from a configuration manager int port = 8448; - server = ServerBUilder.forPort(port) - .addService(new JobServImpl()) + server = ServerBuilder.forPort(port) + .addService(new ShellServerImpl()) .build() .start(); logger.info("Server initialized without tls"); - Runtime.getRuntime().addShutdownHook(new thread() { + Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { // JVM shutdown might break logger functionality @@ -53,7 +53,7 @@ public class JobServServer { } // wrap around stub code generated by GRPC - static class JobServImpl extends JobServ.JobServImplBase { + static class ShellServerImpl extends ShellServerGrpc.ShellServerImplBase { @Override public void getStatusDetail(RequestMessage request, @@ -72,7 +72,6 @@ public class JobServServer { StreamObserver responder) { // TODO: Implement job controller, get output, do the thing OutputMessage reply = OutputMessage.newBuilder() - .setPID(-1) .build(); responder.onNext(reply); responder.onCompleted(); @@ -84,7 +83,7 @@ public class JobServServer { // TODO: Implement job controller, Start Job, do the thing JobStatusMessage reply = JobStatusMessage.newBuilder() .setPID(-1) - .setJobStatus(-1) + .setProcessStatus(-1) .build(); responder.onNext(reply); responder.onCompleted(); From cf5a5f122be6d496f7008027c4924ba7530da415 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 12:05:17 -0700 Subject: [PATCH 07/63] server and client are talking --- build.gradle | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index acfcbd6..57acf4a 100644 --- a/build.gradle +++ b/build.gradle @@ -22,14 +22,12 @@ plugins { id 'application' } -def grpcVersion = '1.22.0-SNAPSHOT' -def protobufVersion = '3.7.1' -def protocVersion = protobufVersion +def grpcVersion = '1.20.0' repositories { - // Use jcenter for resolving your dependencies. - // You can declare any Maven/Ivy/file repository here. - jcenter() + //maven{ url "https://maven-central.storage-download.googleapis.com/repos/central/data/" } + //mavenLocal() + mavenCentral() } dependencies { @@ -43,9 +41,9 @@ dependencies { compile 'org.glassfish:javax.annotation:10.0-b28' // grpc stuff - compile 'io.grpc:grpc-netty-shaded:1.20.0' - compile 'io.grpc:grpc-protobuf:1.20.0' - compile 'io.grpc:grpc-stub:1.20.0' + compile "io.grpc:grpc-netty-shaded:${grpcVersion}" + compile "io.grpc:grpc-protobuf:${grpcVersion}" + compile "io.grpc:grpc-stub:${grpcVersion}" compile 'io.netty:netty-tcnative-boringssl-static:2.0.22.Final' } @@ -59,7 +57,7 @@ protobuf { plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.20.0' + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } @@ -70,10 +68,22 @@ protobuf { } } -apply plugin: 'application' -mainClassName = 'JobServ.JobServClient' -jar { - manifest { - attributes('Main-Class' : mainClassName) - } +task Server(type: CreateStartScripts) { + mainClassName = 'JobServ.JobServServer' + applicationName = 'jobserv-server' + outputDir = new File(project.buildDir, 'tmp') + classpath = startScripts.classpath } + +task Client(type: CreateStartScripts) { + mainClassName = 'JobServ.JobServClient' + applicationName = 'jobserv-client' + outputDir = new File(project.buildDir, 'tmp') + classpath = startScripts.classpath +} + +applicationDistribution.into('bin') { + from(Server) + from(Client) + fileMode = 0755 +} \ No newline at end of file From 2497f848c40fe30c85cb13c8eb3c3bf659e623a1 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 12:19:51 -0700 Subject: [PATCH 08/63] refactored out reentrant code, dont break on bad input (client) --- src/main/java/JobServ/JobServClient.java | 39 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index cbc911a..dfc6ff2 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -153,15 +153,13 @@ public class JobServClient { switch (input) { case "pause": - System.out.println("Enter a PID"); - pid = reader.nextInt(); + pid = getPid(); status = client.makeProcessRequest(pid, PAUSE); // TODO: parse status return break; case "resume": - System.out.println("Enter a PID"); - pid = reader.nextInt(); + pid = getPid(); status = client.makeProcessRequest(pid, RESUME); // TODO: parse status return break; @@ -174,22 +172,19 @@ public class JobServClient { break; case "output": - System.out.println("Enter a PID"); - pid = reader.nextInt(); + pid = getPid(); String out = client.getProcessOutput(pid); System.out.println(out); break; case "status": - System.out.println("Enter a PID"); - pid = reader.nextInt(); + pid = getPid(); status = client.makeProcessRequest(pid, STATUS); System.out.println(String.format("Current status of program is: %d", status)); break; case "return": - System.out.println("Enter a PID"); - pid = reader.nextInt(); + pid = getPid(); status = client.makeProcessRequest(pid, RETURN); System.out.println(String.format("Exit code of process is: %d", status)); break; @@ -199,12 +194,13 @@ public class JobServClient { return; case "help": - System.out.println("pause: pauses a process on the server"); + System.out.println("pause: pauses a process on the server"); System.out.println("resume: resumes a process on the server"); - System.out.println("new: starts a new process on the server"); + System.out.println("new: starts a new process on the server"); System.out.println("output: garners (new) output from a process on the server"); System.out.println("status: outputs the current status of a program on the server"); - System.out.println("return: outputs exit code of a process on the sercer"); + System.out.println("return: outputs exit code of a process on the server"); + System.out.println("quit: exits this server"); break; default: @@ -212,5 +208,22 @@ public class JobServClient { break; } } + + private static int getPid(Scanner reader) { + System.out.println("Enter a PID"); + int pid; + + while(true) { + try { + pid = reader.nextInt(); + + } catch (InputMismatchException e) { + System.out.println("That was not a valid integer"); + continue; + } + + break; + } + } } } From 185b686ad1867fbdd684e9a6859e283e70c81534 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 12:28:22 -0700 Subject: [PATCH 09/63] readme has instructions for building and running --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1983a34..ce98ac9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ # JobServ Remote Procedure Calls over the protobuf API -# Dependancies - # Building +Gradle will manage dependencies, generate code, compile the java, and package the code. +Simply run the folllowing command: +```shell +$ gradle clean build +``` +Gradle will package both the client and server into both a zip and a tarball. +The tarball is located in build/distributions. After extracting one of the archives on a target machine the following commands will run JobServ. +Make sure you are in the directory you extracted the archive to. +```shell +$ ./bin/jobserv-server +$ ./bin/jobserv-client (host) (port) +``` +(TODO: seperate archives for client and server) # Testing - -# Running +(TODO: tests for mtls, job control module) From 68eabf65107a1e8be6d3b7bd467b0d9aa30dd7a5 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Fri, 17 May 2019 18:22:38 -0700 Subject: [PATCH 10/63] refactor API based on design document feedback --- src/main/java/JobServ/JobServClient.java | 292 ++++++++++++++--------- src/main/java/JobServ/JobServServer.java | 46 +++- src/main/proto/jobserv.proto | 32 +-- 3 files changed, 234 insertions(+), 136 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index dfc6ff2..d2170af 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -12,14 +12,6 @@ import java.util.Scanner; // GRPC Client Class public class JobServClient { - // RequestMessage types - // TODO: refactor to enum? - public static final int OUTPUT = 0; - public static final int RETURN = 1; - public static final int PAUSE = 2; - public static final int RESUME = 3; - public static final int STATUS = 4; - /* The client should not use the same logging module as the server. * In a more robust product the server logging module will take advantage of system level * log aggregators such as journalctl, which the client should not be writing to on the users system @@ -52,47 +44,20 @@ public class JobServClient { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } - // sends the server a RequestMessage, blocks until response - // returns an integer representing process status - private int makeProcessRequest(int pid, int requestType) { - logger.info("[+] Sending request"); - - RequestMessage request = RequestMessage.newBuilder() - .setPID(pid) - .setRequestType(requestType) - .build(); - JobStatusMessage response; - - try { - // blocking network operation - response = blockingStub.getStatusDetail(request); - } catch (StatusRuntimeException e) { - /* if this was using an async stub it might be - * worthwhile to include the PID and request type - */ - logger.log(Level.WARNING, "[-]Status Request Failed: %s", e.getStatus()); - return -1; - } - - // return process status - return response.getProcessStatus(); - } - // sends the server a request for output from PID // different from getProcessStatus in output expected from the server // returns process output as string public String getProcessOutput(int pid) { logger.info("[+] requesting output"); - RequestMessage request = RequestMessage.newBuilder() - .setPID(pid) - .setRequestType(OUTPUT) + RequestMessage request = PIDMessage.newBuilder() + .setPid(pid) .build(); OutputMessage response; try { // blocking network operation - response = blockingStub.getJobOutput(request); + response = blockingStub.getOutput(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "[-] Request for output failed: %s", e.getStatus()); // TODO: refactor this out by throwing here and catching in the shell @@ -111,119 +76,226 @@ public class JobServClient { NewJobMessage request = NewJobMessage.newBuilder() .setCommand(command) .build(); - JobStatusMessage response; + PIDMessage response; try { // blocking network operation - response = blockingStub.makeNewJob(request); + response = blockingStub.newJob(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "[-] Request for new job failed!"); return -1; } - if(response.getPID() == -1) { + if(response.getpid() == -1) { logger.log(Level.WARNING, "New job creation failed server side!"); } - return response.getPID(); + return response.getPid(); + } + + // requests running status of job + // returns true if job still running else false + public bool getProcessStatus(int pid) { + logger.info("[+] Requesting status of a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + StatusMessage response; + + try { + // blocking network operation + response = blockingStub.getStatus(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "[-] Request for status failed!"); + } + + return response.getIsRunning(); + } + + // sends PID to server expecting the return cod eof a process + // function returns a 0-255 return code or 277 if still running + // or 278 if error in API + public int getProcessReturn(int pid) { + logger.info("[+] Requesting return code of a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + ReturnMessage response; + + try { + // blocking network operation + response = blockingStub.getReturn(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "[-] Failed to get return code!"); + return 278; + } + + return response.getProcessReturnCode(); + } + + // send a PID to be killed, returns nothing + // logs warning if job status comes back still running + public void killProcess(int pid) { + logger.info("[+] Killing a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + StatusMessage response; + + try { + // blocking network operation + response = blockingStub.killJob(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "[-] Failed to send request!"); + return; + } + + if (response.getIsRunning()) { + logger.log(Level.WARNING, "[-] Server failed to kill job!"); + } } // Client entrypoint public static void main(String[] args) throws Exception { + if (args.length == 1 && args[0] == "--help"){ + outputHelp(); + } + // check args - if(args.length != 3) { - System.out.println("Usage: $ jobservclient host port"); + if (args.length < 3) { + System.out.println("Usage: $ jobservclient host port command"); + System.out.println("Or try client --help") return; } // start client (or fail if port is improperly formatted) JobServClient client; try { - client = new JobServClient(args[1], Integer.parseInt(args[2])); + client = new JobServClient(args[0], Integer.parseInt(args[1])); + } catch (NumberFormatException e) { System.out.println("Invalid Port"); return; } - Scanner reader = new Scanner(System.in); - while(true) { - System.out.print("> "); - int pid, status; - String input = reader.next(); - switch (input) { - - case "pause": - pid = getPid(); - status = client.makeProcessRequest(pid, PAUSE); - // TODO: parse status return - break; - - case "resume": - pid = getPid(); - status = client.makeProcessRequest(pid, RESUME); - // TODO: parse status return - break; - + // declare up here so that multiple switch cases can use it + int candidatePid; + switch (args[2]) { case "new": - System.out.println("Enter a command"); - String command = reader.next(); - pid = client.sendNewJobMessage(command); - System.out.println(String.format("New process on server: %d", pid)); + if (args.length < 4) { + System.out.println("Improper formatting, try client --help"); + break; + } + + String command = ""; + for (int token = 3; i < args.length; i++) { + command += " " + args[token]; + } + + int newProcess = client.sendNewJobMessage(command); + System.out.println("Process started, assigned pid is %d", newProcess); break; case "output": - pid = getPid(); - String out = client.getProcessOutput(pid); - System.out.println(out); + if (args.length != 4) { + System.out.println("Improper formatting, try client --help"); + break; + } + + try { + candidatePid = Integer.parseInt(args[3]); + } catch (InputMismatchException e) { + System.out.println(args[3] + " is not a valid int, much less a valid pid"); + break; + } + + String processOutput = client.getProcessOutput(candidatePid); + System.out.println(processOutput); break; case "status": - pid = getPid(); - status = client.makeProcessRequest(pid, STATUS); - System.out.println(String.format("Current status of program is: %d", status)); + if (args.length != 4) { + System.out.println("Improper formatting, try client --help"); + break; + } + + try { + candidatePid = Integer.parseInt(args[3]); + + } catch (InputMismatchException e) { + System.out.println(args[3] + " is not a valid int, much less a valid pid"); + break; + } + + Boolean processStatus = client.getProcessStatus(candidatePid); + System.out.println("Process is currently running? %b", processStatus); + break; + + case "kill": + if (args.length != 4) { + System.out.println("Improper formatting, try client --help"); + break; + } + + try { + candidatePid = Integer.parseInt(args[3]); + + } catch (InputMismatchException e) { + System.out.println(args[3] + " is not a valid int, much less a valid pid"); + break; + } + + client.getProcessOutput(candidatePid); + System.out.println("End process request recieved!"); break; case "return": - pid = getPid(); - status = client.makeProcessRequest(pid, RETURN); - System.out.println(String.format("Exit code of process is: %d", status)); - break; - - case "quit": - reader.close(); - return; - - case "help": - System.out.println("pause: pauses a process on the server"); - System.out.println("resume: resumes a process on the server"); - System.out.println("new: starts a new process on the server"); - System.out.println("output: garners (new) output from a process on the server"); - System.out.println("status: outputs the current status of a program on the server"); - System.out.println("return: outputs exit code of a process on the server"); - System.out.println("quit: exits this server"); - break; - - default: - System.out.println("Improper output, try 'help'"); - break; - } - } - - private static int getPid(Scanner reader) { - System.out.println("Enter a PID"); - int pid; - - while(true) { - try { - pid = reader.nextInt(); - - } catch (InputMismatchException e) { - System.out.println("That was not a valid integer"); - continue; + if (args.length != 4) { + System.out.println("Improper formatting, try client --help"); + break; } + try { + candidatePid = Integer.parseInt(args[3]); + + } catch (InputMismatchException e) { + System.out.println(args[3] + " is not a valid int, much less a valid pid"); + break; + } + + int returnCode = client.getProcessOutput(candidatePid); + + if (returnCode == 277) { + System.out.println("Process is still running"); + break; + + } else if (returnCode == 278) { + System.out.println("RPC Call error!"); + break; + + } else { + System.out.println("Process Exit Code: %d", returnCode); + } + + default: + System.out.println("Improper command, try 'help'"); break; } } + + public static void outputHelp() { + System.out.println("... new (command)"); + System.out.println("Starts a new process on the server") + System.out.println("... output (pid)"); + System.out.println("Garners output from process on server"); + System.out.println("... status (pid)"); + System.out.println("Returns whether process on server is running"); + System.out.println("... return (pid)"); + System.out.println("Collects return code from remote process"); + System.out.println("... kill (pid)"); + System.out.println("Immediately destroys remote process"); } } diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 985db88..6f0211b 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -56,34 +56,56 @@ public class JobServServer { static class ShellServerImpl extends ShellServerGrpc.ShellServerImplBase { @Override - public void getStatusDetail(RequestMessage request, - StreamObserver responder) { + public void getStatus(PIDMessage request, + StreamObserver responder) { // TODO: Implement job controller, check request type, do the thing - JobStatusMessage reply = JobStatusMessage.newBuilder() - .setPID(-1) - .setProcessStatus(-1) + StatusMessage reply = StatusMessage.newBuilder() + .setStatusCode(-1) .build(); responder.onNext(reply); responder.onCompleted(); } @Override - public void getJobOutput(RequestMessage request, - StreamObserver responder) { + public void getOutput(PIDMessage request, + StreamObserver responder) { // TODO: Implement job controller, get output, do the thing OutputMessage reply = OutputMessage.newBuilder() + .setOutput("test data") .build(); responder.onNext(reply); responder.onCompleted(); } @Override - public void makeNewJob(NewJobMessage request, - StreamObserver responder) { + public void newJob(NewJobMessage request, + StreamObserver responder) { // TODO: Implement job controller, Start Job, do the thing - JobStatusMessage reply = JobStatusMessage.newBuilder() - .setPID(-1) - .setProcessStatus(-1) + PIDMessage reply = PIDMessage.newBuilder() + .setPid(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void getReturn(PIDMessage request, + StreamObserver responder) { + // TODO: Implement job controller, get return code + ReturnMessage reply = ReturnMessage.newBuilder() + .setprocessReturnCode(277) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void killJob(PIDMessage request, + StreamObserver responder) { + // TODO: implement job controller, do the thing + // TODO: kill job here + ReturnMessage reply = ReturnMessage.newBuilder() + .setprocessReturnCode(-1) .build(); responder.onNext(reply); responder.onCompleted(); diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index 5f71bca..da55ece 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -8,25 +8,29 @@ option objc_class_prefix = "JSV"; package JobServ; service ShellServer { - rpc getStatusDetail (RequestMessage) returns (JobStatusMessage) {} - rpc getJobOutput (RequestMessage) returns (OutputMessage) {} - rpc makeNewJob (NewJobMessage) returns (JobStatusMessage) {} + rpc getStatus (PIDMessage) returns (StatusMessage) {} + rpc getReturn (PIDMessage) returns (ReturnMessage) {} + rpc getOutput (PIDMessage) returns (OutputMessage) {} + rpc killJob (PIDMessage) returns (StatusMessage) {} + rpc newJob (NewJobMessage) returns (PIDMessage) {} } -message RequestMessage { - int32 PID = 1; - int32 RequestType = 2; +message StatusMessage { + bool IsRunning = 1 } -message NewJobMessage { - string Command = 1; -} - -message JobStatusMessage { - int32 PID = 1; // linux PIDs, by default, go up to 2^22 - int32 ProcessStatus = 2; // TODO: figure out smallest possible data size for this +message ReturnMessage { + int32 ProcessReturnCode = 1; } message OutputMessage { - string ProcessOutput = 2; + string Output = 1; +} + +message NewJobMessage { + string Command = 1; +} + +message PIDMessage { + int32 Pid = 1; } From 05eac44a4cbd6b0f6fb908a28c639ae0cdd6a343 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 12:20:12 -0700 Subject: [PATCH 11/63] fixed my night crew typoes --- src/main/java/JobServ/JobServClient.java | 30 ++++++++++++------------ src/main/java/JobServ/JobServServer.java | 12 +++++----- src/main/proto/jobserv.proto | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index d2170af..29c6c18 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -3,7 +3,7 @@ package JobServ; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; - +import java.util.InputMismatchException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,7 +50,7 @@ public class JobServClient { public String getProcessOutput(int pid) { logger.info("[+] requesting output"); - RequestMessage request = PIDMessage.newBuilder() + PIDMessage request = PIDMessage.newBuilder() .setPid(pid) .build(); OutputMessage response; @@ -60,11 +60,10 @@ public class JobServClient { response = blockingStub.getOutput(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "[-] Request for output failed: %s", e.getStatus()); - // TODO: refactor this out by throwing here and catching in the shell - return ""; + return ""; } - return response.getProcessOutput(); + return response.getOutput(); } // sends the server a command for a new job, blocks until response @@ -86,7 +85,7 @@ public class JobServClient { return -1; } - if(response.getpid() == -1) { + if(response.getPid() == -1) { logger.log(Level.WARNING, "New job creation failed server side!"); } @@ -95,7 +94,7 @@ public class JobServClient { // requests running status of job // returns true if job still running else false - public bool getProcessStatus(int pid) { + public Boolean getProcessStatus(int pid) { logger.info("[+] Requesting status of a job"); PIDMessage request = PIDMessage.newBuilder() @@ -108,6 +107,7 @@ public class JobServClient { response = blockingStub.getStatus(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "[-] Request for status failed!"); + return false; } return response.getIsRunning(); @@ -167,7 +167,7 @@ public class JobServClient { // check args if (args.length < 3) { System.out.println("Usage: $ jobservclient host port command"); - System.out.println("Or try client --help") + System.out.println("Or try client --help"); return; } @@ -191,12 +191,12 @@ public class JobServClient { } String command = ""; - for (int token = 3; i < args.length; i++) { + for (int token = 3; token < args.length; token++) { command += " " + args[token]; } int newProcess = client.sendNewJobMessage(command); - System.out.println("Process started, assigned pid is %d", newProcess); + System.out.printf("Process started, assigned pid is %d\n", newProcess); break; case "output": @@ -231,7 +231,7 @@ public class JobServClient { } Boolean processStatus = client.getProcessStatus(candidatePid); - System.out.println("Process is currently running? %b", processStatus); + System.out.printf("Process is currently running? %b\n", processStatus); break; case "kill": @@ -248,7 +248,7 @@ public class JobServClient { break; } - client.getProcessOutput(candidatePid); + client.killProcess(candidatePid); System.out.println("End process request recieved!"); break; @@ -266,7 +266,7 @@ public class JobServClient { break; } - int returnCode = client.getProcessOutput(candidatePid); + int returnCode = client.getProcessReturn(candidatePid); if (returnCode == 277) { System.out.println("Process is still running"); @@ -277,7 +277,7 @@ public class JobServClient { break; } else { - System.out.println("Process Exit Code: %d", returnCode); + System.out.printf("Process Exit Code: %d\n", returnCode); } default: @@ -288,7 +288,7 @@ public class JobServClient { public static void outputHelp() { System.out.println("... new (command)"); - System.out.println("Starts a new process on the server") + System.out.println("Starts a new process on the server"); System.out.println("... output (pid)"); System.out.println("Garners output from process on server"); System.out.println("... status (pid)"); diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 6f0211b..20350fd 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -37,7 +37,7 @@ public class JobServServer { server.shutdown(); } } - + // spinlock for main() private void blockUntilShutdown() throws InterruptedException { if (server != null) { @@ -60,7 +60,7 @@ public class JobServServer { StreamObserver responder) { // TODO: Implement job controller, check request type, do the thing StatusMessage reply = StatusMessage.newBuilder() - .setStatusCode(-1) + .setIsRunning(true) .build(); responder.onNext(reply); responder.onCompleted(); @@ -93,7 +93,7 @@ public class JobServServer { StreamObserver responder) { // TODO: Implement job controller, get return code ReturnMessage reply = ReturnMessage.newBuilder() - .setprocessReturnCode(277) + .setProcessReturnCode(277) .build(); responder.onNext(reply); responder.onCompleted(); @@ -101,11 +101,11 @@ public class JobServServer { @Override public void killJob(PIDMessage request, - StreamObserver responder) { + StreamObserver responder) { // TODO: implement job controller, do the thing // TODO: kill job here - ReturnMessage reply = ReturnMessage.newBuilder() - .setprocessReturnCode(-1) + StatusMessage reply = StatusMessage.newBuilder() + .setIsRunning(false) .build(); responder.onNext(reply); responder.onCompleted(); diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index da55ece..e223cd0 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -16,7 +16,7 @@ service ShellServer { } message StatusMessage { - bool IsRunning = 1 + bool IsRunning = 1; } message ReturnMessage { From 38e89d628bec85f1a24920ca180e3e084c9e08ed Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 13:05:02 -0700 Subject: [PATCH 12/63] class and file docs --- src/main/java/JobServ/JobServClient.java | 2 +- src/main/java/JobServ/JobServServer.java | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 29c6c18..79032ab 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -160,7 +160,7 @@ public class JobServClient { // Client entrypoint public static void main(String[] args) throws Exception { - if (args.length == 1 && args[0] == "--help"){ + if (args.length == 1 && args[0] == "help"){ outputHelp(); } diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 20350fd..682da01 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -1,3 +1,11 @@ +/* + * JobServServer + * + * v1.0 + * + * May 18, 2019 + */ + package JobServ; import io.grpc.Server; @@ -7,6 +15,13 @@ import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.logging.Logger; + +/* + * The JobServServer class implements the JobServ protobuf API + * It does this by extending the gRPC stub code. + * Additionally, JobServServer starts and manages a daemon + * Which accepts incoming connections from client. + */ public class JobServServer { private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); @@ -54,7 +69,7 @@ public class JobServServer { // wrap around stub code generated by GRPC static class ShellServerImpl extends ShellServerGrpc.ShellServerImplBase { - + @Override public void getStatus(PIDMessage request, StreamObserver responder) { @@ -65,7 +80,7 @@ public class JobServServer { responder.onNext(reply); responder.onCompleted(); } - + @Override public void getOutput(PIDMessage request, StreamObserver responder) { @@ -76,7 +91,7 @@ public class JobServServer { responder.onNext(reply); responder.onCompleted(); } - + @Override public void newJob(NewJobMessage request, StreamObserver responder) { From da6aa04f45db35e1a9dbefdaa1b087173662669f Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 13:08:09 -0700 Subject: [PATCH 13/63] added class and file documentation to client class --- src/main/java/JobServ/JobServClient.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 79032ab..b3df54f 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -1,3 +1,11 @@ +/* + * JobServClient + * + * v1.0 + * + * May 18, 2019 + */ + package JobServ; import io.grpc.ManagedChannel; @@ -9,7 +17,10 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.Scanner; -// GRPC Client Class +/* + * The JobServClient class extends the gRPC stub code + * Additionally, it plugs a command line interface into the API code. + */ public class JobServClient { /* The client should not use the same logging module as the server. From c855151af8f121e29ff5c3207bb5876e0e029014 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 15:28:36 -0700 Subject: [PATCH 14/63] server side tls code --- src/main/java/JobServ/JobServServer.java | 45 +++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 682da01..2230bf4 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -11,11 +11,15 @@ package JobServ; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyServerBuilder; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; import java.io.IOException; import java.util.logging.Logger; - /* * The JobServServer class implements the JobServ protobuf API * It does this by extending the gRPC stub code. @@ -26,16 +30,40 @@ public class JobServServer { private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); private Server server; + private final int port; + private final String certChainFilePath; + private final String privateKeyFilePath; + private final String trustCertCollectionFilePath; + + public JobServServer(int port, + String certChainFilePath, + String privateKeyFilePath, + String trustCertCollectionFilePath) { + this.port = port; + this.certChainFilePath = certChainFilePath; + this.privateKeyFilePath = privateKeyFilePath; + this.trustCertCollectionFilePath = trustCertCollectionFilePath; + } + + private SslContextBuilder getSslContextBuilder() { + SslContextBuilder sslClientContextBuilder = SslContextBuilder.forServer(new File(certChainFilePath), + new File(privateKeyFilePath)); + + sslClientContextBuilder.trustManager(new File(trustCertCollectionFilePath)); + sslClientContextBuilder.clientAuth(ClientAuth.REQUIRE); + + return GrpcSslContexts.configure(sslClientContextBuilder); // starts the GRPC API Server private void start() throws IOException { // TODO: this should be passed in from a configuration manager - int port = 8448; - server = ServerBuilder.forPort(port) + server = ServerBuilder.forPort(this.port) .addService(new ShellServerImpl()) + .sslContext(getSslContextBuilder().build()) .build() .start(); - logger.info("Server initialized without tls"); + logger.info("Server initialized!"); + Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { @@ -62,7 +90,14 @@ public class JobServServer { // Main function. starts GRPC server and spins until server is shutdown public static void main(String[] args) throws IOException, InterruptedException { - final JobServServer server = new JobServServer(); + int port = 8448; // TODO: port and key/cert files should be handled by a config manager + + if(args.length < 3) { + System.out.println("Usage: ./jobserv-server certchain privatekey truststore"); + return; + } + + final JobServServer server = new JobServServer(8448, args[0], args[1], args[2]); server.start(); server.blockUntilShutdown(); } From f45a96c1e071e2d1efeacc1ff61b16910272bd7e Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 16:40:58 -0700 Subject: [PATCH 15/63] refactored out private module of JobServServer into its own module. got ssl code building --- build.gradle | 2 +- src/main/java/JobServ/JobServServer.java | 69 ++---------------- src/main/java/JobServ/ShellServerService.java | 73 +++++++++++++++++++ 3 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 src/main/java/JobServ/ShellServerService.java diff --git a/build.gradle b/build.gradle index 57acf4a..e3d3779 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ dependencies { compile 'org.glassfish:javax.annotation:10.0-b28' // grpc stuff - compile "io.grpc:grpc-netty-shaded:${grpcVersion}" + compile "io.grpc:grpc-netty:${grpcVersion}" compile "io.grpc:grpc-protobuf:${grpcVersion}" compile "io.grpc:grpc-stub:${grpcVersion}" compile 'io.netty:netty-tcnative-boringssl-static:2.0.22.Final' diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 2230bf4..a3b6021 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -10,13 +10,13 @@ package JobServ; import io.grpc.Server; import io.grpc.ServerBuilder; -import io.grpc.stub.StreamObserver; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; +import java.io.File; import java.io.IOException; import java.util.logging.Logger; @@ -53,12 +53,13 @@ public class JobServServer { sslClientContextBuilder.clientAuth(ClientAuth.REQUIRE); return GrpcSslContexts.configure(sslClientContextBuilder); + } // starts the GRPC API Server private void start() throws IOException { // TODO: this should be passed in from a configuration manager - server = ServerBuilder.forPort(this.port) - .addService(new ShellServerImpl()) + server = NettyServerBuilder.forPort(port) + .addService(new ShellServerService()) .sslContext(getSslContextBuilder().build()) .build() .start(); @@ -90,7 +91,7 @@ public class JobServServer { // Main function. starts GRPC server and spins until server is shutdown public static void main(String[] args) throws IOException, InterruptedException { - int port = 8448; // TODO: port and key/cert files should be handled by a config manager + int port = 8448; // TODO: port and key/cert files should be handled by a config manager if(args.length < 3) { System.out.println("Usage: ./jobserv-server certchain privatekey truststore"); @@ -101,64 +102,4 @@ public class JobServServer { server.start(); server.blockUntilShutdown(); } - - // wrap around stub code generated by GRPC - static class ShellServerImpl extends ShellServerGrpc.ShellServerImplBase { - - @Override - public void getStatus(PIDMessage request, - StreamObserver responder) { - // TODO: Implement job controller, check request type, do the thing - StatusMessage reply = StatusMessage.newBuilder() - .setIsRunning(true) - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void getOutput(PIDMessage request, - StreamObserver responder) { - // TODO: Implement job controller, get output, do the thing - OutputMessage reply = OutputMessage.newBuilder() - .setOutput("test data") - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void newJob(NewJobMessage request, - StreamObserver responder) { - // TODO: Implement job controller, Start Job, do the thing - PIDMessage reply = PIDMessage.newBuilder() - .setPid(-1) - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void getReturn(PIDMessage request, - StreamObserver responder) { - // TODO: Implement job controller, get return code - ReturnMessage reply = ReturnMessage.newBuilder() - .setProcessReturnCode(277) - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - - @Override - public void killJob(PIDMessage request, - StreamObserver responder) { - // TODO: implement job controller, do the thing - // TODO: kill job here - StatusMessage reply = StatusMessage.newBuilder() - .setIsRunning(false) - .build(); - responder.onNext(reply); - responder.onCompleted(); - } - } } diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java new file mode 100644 index 0000000..f6cb394 --- /dev/null +++ b/src/main/java/JobServ/ShellServerService.java @@ -0,0 +1,73 @@ +/* + * ShellServerService + * + * v1.0 + * + * May 18, 2019 + */ + +package JobServ; +import io.grpc.stub.StreamObserver; + +/* + * The ShellServerService wraps around the protobuf API + * Implements API endpoints + */ +class ShellServerService extends ShellServerGrpc.ShellServerImplBase { + + @Override + public void getStatus(PIDMessage request, + StreamObserver responder) { + // TODO: Implement job controller, check request type, do the thing + StatusMessage reply = StatusMessage.newBuilder() + .setIsRunning(true) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void getOutput(PIDMessage request, + StreamObserver responder) { + // TODO: Implement job controller, get output, do the thing + OutputMessage reply = OutputMessage.newBuilder() + .setOutput("test data") + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void newJob(NewJobMessage request, + StreamObserver responder) { + // TODO: Implement job controller, Start Job, do the thing + PIDMessage reply = PIDMessage.newBuilder() + .setPid(-1) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void getReturn(PIDMessage request, + StreamObserver responder) { + // TODO: Implement job controller, get return code + ReturnMessage reply = ReturnMessage.newBuilder() + .setProcessReturnCode(277) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } + + @Override + public void killJob(PIDMessage request, + StreamObserver responder) { + // TODO: implement job controller, do the thing + // TODO: kill job here + StatusMessage reply = StatusMessage.newBuilder() + .setIsRunning(false) + .build(); + responder.onNext(reply); + responder.onCompleted(); + } +} \ No newline at end of file From c91ddb9f2e9fd9e8df1f02e9758249e400fa0875 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 18 May 2019 20:25:34 -0700 Subject: [PATCH 16/63] script wrapping build process managing certificates and triggering a build --- .gitignore | 5 +++- buildwrapper.sh | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100755 buildwrapper.sh diff --git a/.gitignore b/.gitignore index f2d1c84..fecc8c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ build # Ignore emacs swapfiles \#* -.\#* \ No newline at end of file +.\#* + +# Dont commit certs +resources/* diff --git a/buildwrapper.sh b/buildwrapper.sh new file mode 100755 index 0000000..d83e590 --- /dev/null +++ b/buildwrapper.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +SERVER_CA_CN=jobserv-server-ca +SERVER_CN=jobserv-server +SERVER_PATH=resources/server +CLIENT_CA_CN=jobserv-client-ca +CLIENT_CN=jobserv-client +CLIENT_PATH=resources/client +TEST_PATH=resources/test + +rm -rf resources/* +mkdir resources/client +mkdir resources/server +mkdir resources/test + + +# Get passwords for CAs +read -p "Enter Server CA Passphrase: " SRVCAPASS +read -p "Enter Client CA Passphrase: " CLTCAPASS + +# Generate CA Keys +echo "[+] Generating Server CA Key" +openssl genrsa -passout pass:$SRVCAPASS -aes256 -out $SERVER_PATH/ca.key 4096 +echo "[+] Generating Client CA Key" +openssl genrsa -passout pass:$CLTCAPASS -aes256 -out $CLIENT_PATH/ca.key 4096 +echo "[+] Generating test CA Key" +openssl genrsa -passout pass:dontusethiskey -aes256 -out $TEST_PATH/ca.key 4096 + +# Generate CA Certs +echo "[+] Generating Server CA Cert" +openssl req -passin pass:$SRVCAPASS -new -x509 -days 365 -key $SERVER_PATH/ca.key -out $SERVER_PATH/ca.crt -subj "/CN=${SERVER_CA_CN}" +echo "[+] Generating Client CA Cert" +openssl req -passin pass:$CLTCAPASS -new -x509 -days 365 -key $CLIENT_PATH/ca.key -out $CLIENT_PATH/ca.crt -subj "/CN=${CLIENT_CA_CN}" +echo "[+] Generating test CA Key" +openssl req -passin pass:dontusethiskey -new -x509 -days 365 -key $TEST_PATH/ca.key -out $TEST_PATH/ca.crt -subj "/CN=DontUseMe" + + +# Generate Server Key, Signing request, cert +echo "[+] Generating Server key" +openssl genrsa -passout pass:${SRVCAPASS} -aes256 -out $SERVER_PATH/private.key 4096 +echo "[+] Generating Server signing request" +openssl req -passin pass:${SRVCAPASS} -out $SERVER_PATH/request.csr -subj "/CN=${SERVER_CN}" +echo "[+] Generating Server certificate " +openssl x509 -req -passin pass:${SRVCAPASS} -days 365 -in $SERVER_PATH/request.csr -CA $SERVER_PATH/ca.crt -CAkey $SERVER_PATH/ca.key -set_serial 01 -out $SERVER_PATH/server.crt +echo "[+] Removing passphrase from server key" +openssl rsa -passin pass:${SRVCAPASS} -in $SERVER_PATH/private.key -out $SERVER_PATH/private.key + +# Generate Client Key, Signing request, cert +echo "[+] Generating Client key" +openssl genrsa -passout pass:${CLTCAPASS} -aes256 -out $CLIENT_PATH/private.key 4096 +echo "[+] Generating Client signing request" +openssl req -passin pass:${CLTCAPASS} -out $CLIENT_PATH/request.csr -subj "/CN=${CLIENT_CN}" +echo "[+] Generating Client certificate " +openssl x509 -req -passin pass:${CLTCAPASS} -days 365 -in $CLIENT_PATH/request.csr -CA $CLIENT_PATH/ca.crt -CAkey $CLIENT_PATH/ca.key -set_serial 01 -out $CLIENT_PATH/server.crt +echo "[+] Removing passphrase from client key" +openssl rsa -passin pass:${CLTCAPASS} -in $CLIENT_PATH/private.key -out $CLIENT_PATH/private.key + +# Generate Test Key, Signing request, cert +echo "[+] Generating server key" +openssl genrsa -passout pass:dontusethiskey -aes256 -out $TEST_PATH/private.key 4096 +echo "[+] Generating server signing request" +openssl req -passin pass:dontusethiskey -out $TEST_PATH/request.csr -subj "/CN=${DontUseMe}" +echo "[+] Generating server certificate " +openssl x509 -req -passin pass:dontusethiskey -days 365 -in $TEST_PATH/request.csr -CA $TEST_PATH/ca.crt -CAkey $TEST_PATH/ca.key -set_serial 01 -out $TEST_PATH/server.crt +echo "[+] Removing passphrase from test key" +openssl rsa -passin pass:dontusethiskey -in $TEST_PATH/private.key -out $TEST_PATH/private.key + + +echo "[+] Converting private keys to X.509" +openssl pkcs8 -topk8 -nocrypt -in $CLIENT_PATH/private.key -out $CLIENT_PATH/private.pem +openssl pkcs8 -topk8 -nocrypt -in $SERVER_PATH/private.key -out $SERVER_PATH/private.pem +openssl pkcs8 -topk8 -nocrypt -in $TEST_PATH/private.key -out $TEST_PATH/private.pem + +echo "[+] creating combine trust store" +cat $SERVER_PATH/ca.crt $CLIENT_PATH/ca.crt > resources/truststore.pem + +echo "[+] initiating gradle build" +./gradlew clear build From 5d66a9880cf193b9c5161e3595e1ae90f334f232 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 11:37:58 -0700 Subject: [PATCH 17/63] pass in private key for generation of cert requests --- buildwrapper.sh | 12 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++++++++++++++++ gradlew.bat | 84 +++++++++++ 5 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/buildwrapper.sh b/buildwrapper.sh index d83e590..265b90f 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -39,7 +39,7 @@ openssl req -passin pass:dontusethiskey -new -x509 -days 365 -key $TEST_PATH/ca. echo "[+] Generating Server key" openssl genrsa -passout pass:${SRVCAPASS} -aes256 -out $SERVER_PATH/private.key 4096 echo "[+] Generating Server signing request" -openssl req -passin pass:${SRVCAPASS} -out $SERVER_PATH/request.csr -subj "/CN=${SERVER_CN}" +openssl req -passin pass:${SRVCAPASS} -new -key $SERVER_PATH/private.key -out $SERVER_PATH/request.csr -subj "/CN=${SERVER_CN}" echo "[+] Generating Server certificate " openssl x509 -req -passin pass:${SRVCAPASS} -days 365 -in $SERVER_PATH/request.csr -CA $SERVER_PATH/ca.crt -CAkey $SERVER_PATH/ca.key -set_serial 01 -out $SERVER_PATH/server.crt echo "[+] Removing passphrase from server key" @@ -49,18 +49,18 @@ openssl rsa -passin pass:${SRVCAPASS} -in $SERVER_PATH/private.key -out $SERVER_ echo "[+] Generating Client key" openssl genrsa -passout pass:${CLTCAPASS} -aes256 -out $CLIENT_PATH/private.key 4096 echo "[+] Generating Client signing request" -openssl req -passin pass:${CLTCAPASS} -out $CLIENT_PATH/request.csr -subj "/CN=${CLIENT_CN}" +openssl req -passin pass:${CLTCAPASS} -new -key $CLIENT_PATH/private.key -out $CLIENT_PATH/request.csr -subj "/CN=${CLIENT_CN}" echo "[+] Generating Client certificate " openssl x509 -req -passin pass:${CLTCAPASS} -days 365 -in $CLIENT_PATH/request.csr -CA $CLIENT_PATH/ca.crt -CAkey $CLIENT_PATH/ca.key -set_serial 01 -out $CLIENT_PATH/server.crt echo "[+] Removing passphrase from client key" openssl rsa -passin pass:${CLTCAPASS} -in $CLIENT_PATH/private.key -out $CLIENT_PATH/private.key # Generate Test Key, Signing request, cert -echo "[+] Generating server key" +echo "[+] Generating test key" openssl genrsa -passout pass:dontusethiskey -aes256 -out $TEST_PATH/private.key 4096 -echo "[+] Generating server signing request" -openssl req -passin pass:dontusethiskey -out $TEST_PATH/request.csr -subj "/CN=${DontUseMe}" -echo "[+] Generating server certificate " +echo "[+] Generating test signing request" +openssl req -passin pass:dontusethiskey -new -key $TEST_PATH/private.key -out $TEST_PATH/request.csr -subj "/CN=${DontUseMe}" +echo "[+] Generating test certificate " openssl x509 -req -passin pass:dontusethiskey -days 365 -in $TEST_PATH/request.csr -CA $TEST_PATH/ca.crt -CAkey $TEST_PATH/ca.key -set_serial 01 -out $TEST_PATH/server.crt echo "[+] Removing passphrase from test key" openssl rsa -passin pass:dontusethiskey -in $TEST_PATH/private.key -out $TEST_PATH/private.key diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem 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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 3021a1d40526f858538c966c86a1e6f5e7603d57 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 12:21:00 -0700 Subject: [PATCH 18/63] added tls code to client --- buildwrapper.sh | 6 +- src/main/java/JobServ/JobServClient.java | 141 +++++++++++++++-------- 2 files changed, 96 insertions(+), 51 deletions(-) diff --git a/buildwrapper.sh b/buildwrapper.sh index 265b90f..4526b8b 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -51,7 +51,7 @@ openssl genrsa -passout pass:${CLTCAPASS} -aes256 -out $CLIENT_PATH/private.key echo "[+] Generating Client signing request" openssl req -passin pass:${CLTCAPASS} -new -key $CLIENT_PATH/private.key -out $CLIENT_PATH/request.csr -subj "/CN=${CLIENT_CN}" echo "[+] Generating Client certificate " -openssl x509 -req -passin pass:${CLTCAPASS} -days 365 -in $CLIENT_PATH/request.csr -CA $CLIENT_PATH/ca.crt -CAkey $CLIENT_PATH/ca.key -set_serial 01 -out $CLIENT_PATH/server.crt +openssl x509 -req -passin pass:${CLTCAPASS} -days 365 -in $CLIENT_PATH/request.csr -CA $CLIENT_PATH/ca.crt -CAkey $CLIENT_PATH/ca.key -set_serial 01 -out $CLIENT_PATH/client.crt echo "[+] Removing passphrase from client key" openssl rsa -passin pass:${CLTCAPASS} -in $CLIENT_PATH/private.key -out $CLIENT_PATH/private.key @@ -61,7 +61,7 @@ openssl genrsa -passout pass:dontusethiskey -aes256 -out $TEST_PATH/private.key echo "[+] Generating test signing request" openssl req -passin pass:dontusethiskey -new -key $TEST_PATH/private.key -out $TEST_PATH/request.csr -subj "/CN=${DontUseMe}" echo "[+] Generating test certificate " -openssl x509 -req -passin pass:dontusethiskey -days 365 -in $TEST_PATH/request.csr -CA $TEST_PATH/ca.crt -CAkey $TEST_PATH/ca.key -set_serial 01 -out $TEST_PATH/server.crt +openssl x509 -req -passin pass:dontusethiskey -days 365 -in $TEST_PATH/request.csr -CA $TEST_PATH/ca.crt -CAkey $TEST_PATH/ca.key -set_serial 01 -out $TEST_PATH/test.crt echo "[+] Removing passphrase from test key" openssl rsa -passin pass:dontusethiskey -in $TEST_PATH/private.key -out $TEST_PATH/private.key @@ -75,4 +75,4 @@ echo "[+] creating combine trust store" cat $SERVER_PATH/ca.crt $CLIENT_PATH/ca.crt > resources/truststore.pem echo "[+] initiating gradle build" -./gradlew clear build +./gradlew clean build diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index b3df54f..b56c409 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -11,6 +11,13 @@ package JobServ; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +import javax.net.ssl.SSLException; +import java.io.File; import java.util.InputMismatchException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -23,7 +30,8 @@ import java.util.Scanner; */ public class JobServClient { - /* The client should not use the same logging module as the server. + /* + * The client should not use the same logging module as the server. * In a more robust product the server logging module will take advantage of system level * log aggregators such as journalctl, which the client should not be writing to on the users system */ @@ -31,33 +39,49 @@ public class JobServClient { private final ManagedChannel channel; - /* blockingStub is used when the client needs to block until the server responds + /* + * blockingStub is used when the client needs to block until the server responds * the client doesnt nessesarily need to support asynchronously firing off commands - * in this shell-like interface it would be disconcerting to get multiple returns out of order + * in this shell-like interface it would be disconcerting to get multiple returns out of order */ private final ShellServerGrpc.ShellServerBlockingStub blockingStub; - // Constructor connects to server - public JobServClient(String host, int port) { - this(ManagedChannelBuilder.forAddress(host, port) - // TODO: MTLS - .usePlaintext() - .build()); - } - - // private overload of constructor, used in the above constructor - JobServClient(ManagedChannel channel) { - this.channel = channel; - blockingStub = ShellServerGrpc.newBlockingStub(channel); + /* + * Constructor + * Creates an SslContext from cert, key, and trust store + * Creates a ManagedChannel object from SSL Parameters + * Spawns a new blockingStub for network operations with the server + */ + public JobServClient(String host, + int port, + String trustStore, + String clientCert, + String clientPrivateKey) throws SSLException { + SslContextBuilder builder = GrpcSslContexts.forClient(); + builder.trustManager(new File(trustStore)); + builder.keyManager(new File(clientCert), new File(clientPrivateKey)); + + this.channel = NettyChannelBuilder.forAddress(host, port) + .sslContext(builder.build()) + .build(); + + blockingStub = ShellServerGrpc.newBlockingStub(this.channel); } + /* + * shutdown() + * Gets called when you press cntrl+c + * takes at most 5 seconds to close its connection + */ public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } - // sends the server a request for output from PID - // different from getProcessStatus in output expected from the server - // returns process output as string + /* + * getProcessInfo() + * sends the server a request for output from the process identified by 'pid' + * returns process output as string + */ public String getProcessOutput(int pid) { logger.info("[+] requesting output"); @@ -77,8 +101,11 @@ public class JobServClient { return response.getOutput(); } - // sends the server a command for a new job, blocks until response - // returns new pid of job + /* + * sendNewJobMessage() + * sends a shell command to the api server + * returns new pid of job + */ public int sendNewJobMessage(String command) { // thought of escaping this, but the vulnerability is only client side, from client user input. logger.info("[+] Sending command to server"); @@ -103,8 +130,11 @@ public class JobServClient { return response.getPid(); } - // requests running status of job - // returns true if job still running else false + /* + * getProcessStatus() + * requests running status of process pid + * returns true if process still running else false + */ public Boolean getProcessStatus(int pid) { logger.info("[+] Requesting status of a job"); @@ -124,9 +154,12 @@ public class JobServClient { return response.getIsRunning(); } - // sends PID to server expecting the return cod eof a process - // function returns a 0-255 return code or 277 if still running - // or 278 if error in API + /* + * sends PID to server + * returns process exit code + * returns a 0-255 return code or 277 if still running + * or 278 if error in API + */ public int getProcessReturn(int pid) { logger.info("[+] Requesting return code of a job"); @@ -146,8 +179,11 @@ public class JobServClient { return response.getProcessReturnCode(); } - // send a PID to be killed, returns nothing - // logs warning if job status comes back still running + /* + * killProcess() + * send a PID to be killed, function returns nothing + * logs warning if job status comes back still running + */ public void killProcess(int pid) { logger.info("[+] Killing a job"); @@ -169,40 +205,49 @@ public class JobServClient { } } - // Client entrypoint + /* + * main() + * Client entrypoint + * Parses arguments and calls the correct function + */ public static void main(String[] args) throws Exception { if (args.length == 1 && args[0] == "help"){ outputHelp(); } // check args - if (args.length < 3) { - System.out.println("Usage: $ jobservclient host port command"); - System.out.println("Or try client --help"); + if (args.length < 7) { + System.out.println("Usage: $ ./jobserv-client privatekey, cert, truststore, host, port, command, args"); + System.out.println("Or try $ ./jobserv-client help"); return; } - // start client (or fail if port is improperly formatted) + // start client + // fails if port is improperly formatted or if an ssl exception occurs JobServClient client; try { - client = new JobServClient(args[0], Integer.parseInt(args[1])); + client = new JobServClient(args[0], Integer.parseInt(args[1]), args[2], args[1], args[0]); } catch (NumberFormatException e) { System.out.println("Invalid Port"); return; + } catch (SSLException e) { + System.out.println(e.getMessage()); + return; } - // declare up here so that multiple switch cases can use it + // declare pid up here so that multiple switch cases can use it int candidatePid; + // parse remaining args switch (args[2]) { case "new": - if (args.length < 4) { + if (args.length < 7) { System.out.println("Improper formatting, try client --help"); break; } String command = ""; - for (int token = 3; token < args.length; token++) { + for (int token = 6; token < args.length; token++) { command += " " + args[token]; } @@ -211,15 +256,15 @@ public class JobServClient { break; case "output": - if (args.length != 4) { + if (args.length != 7) { System.out.println("Improper formatting, try client --help"); break; } try { - candidatePid = Integer.parseInt(args[3]); + candidatePid = Integer.parseInt(args[6]); } catch (InputMismatchException e) { - System.out.println(args[3] + " is not a valid int, much less a valid pid"); + System.out.println(args[6] + " is not a valid int, much less a valid pid"); break; } @@ -228,16 +273,16 @@ public class JobServClient { break; case "status": - if (args.length != 4) { + if (args.length != 7) { System.out.println("Improper formatting, try client --help"); break; } try { - candidatePid = Integer.parseInt(args[3]); + candidatePid = Integer.parseInt(args[6]); } catch (InputMismatchException e) { - System.out.println(args[3] + " is not a valid int, much less a valid pid"); + System.out.println(args[6] + " is not a valid int, much less a valid pid"); break; } @@ -246,16 +291,16 @@ public class JobServClient { break; case "kill": - if (args.length != 4) { + if (args.length != 7) { System.out.println("Improper formatting, try client --help"); break; } try { - candidatePid = Integer.parseInt(args[3]); + candidatePid = Integer.parseInt(args[6]); } catch (InputMismatchException e) { - System.out.println(args[3] + " is not a valid int, much less a valid pid"); + System.out.println(args[6] + " is not a valid int, much less a valid pid"); break; } @@ -264,16 +309,16 @@ public class JobServClient { break; case "return": - if (args.length != 4) { + if (args.length != 7) { System.out.println("Improper formatting, try client --help"); break; } try { - candidatePid = Integer.parseInt(args[3]); + candidatePid = Integer.parseInt(args[6]); } catch (InputMismatchException e) { - System.out.println(args[3] + " is not a valid int, much less a valid pid"); + System.out.println(args[6] + " is not a valid int, much less a valid pid"); break; } From a932852b2cb0ea9412b013bcb9476cb62c2c2d72 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 13:03:53 -0700 Subject: [PATCH 19/63] refactors to server tls code --- README.md | 3 ++ buildwrapper.sh | 6 ++- src/main/java/JobServ/JobServClient.java | 4 +- src/main/java/JobServ/JobServServer.java | 58 ++++++++++++++---------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ce98ac9..053c311 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # JobServ Remote Procedure Calls over the protobuf API +# Requirements +- openssl + # Building Gradle will manage dependencies, generate code, compile the java, and package the code. Simply run the folllowing command: diff --git a/buildwrapper.sh b/buildwrapper.sh index 4526b8b..85132ac 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -6,6 +6,8 @@ SERVER_PATH=resources/server CLIENT_CA_CN=jobserv-client-ca CLIENT_CN=jobserv-client CLIENT_PATH=resources/client +TEST_CA_CN=jobserv-bad-cert-ca +TEST_CN=jobserv-bad-cert TEST_PATH=resources/test rm -rf resources/* @@ -32,7 +34,7 @@ openssl req -passin pass:$SRVCAPASS -new -x509 -days 365 -key $SERVER_PATH/ca.ke echo "[+] Generating Client CA Cert" openssl req -passin pass:$CLTCAPASS -new -x509 -days 365 -key $CLIENT_PATH/ca.key -out $CLIENT_PATH/ca.crt -subj "/CN=${CLIENT_CA_CN}" echo "[+] Generating test CA Key" -openssl req -passin pass:dontusethiskey -new -x509 -days 365 -key $TEST_PATH/ca.key -out $TEST_PATH/ca.crt -subj "/CN=DontUseMe" +openssl req -passin pass:dontusethiskey -new -x509 -days 365 -key $TEST_PATH/ca.key -out $TEST_PATH/ca.crt -subj "/CN=${TEST_CA_CN}" # Generate Server Key, Signing request, cert @@ -59,7 +61,7 @@ openssl rsa -passin pass:${CLTCAPASS} -in $CLIENT_PATH/private.key -out $CLIENT_ echo "[+] Generating test key" openssl genrsa -passout pass:dontusethiskey -aes256 -out $TEST_PATH/private.key 4096 echo "[+] Generating test signing request" -openssl req -passin pass:dontusethiskey -new -key $TEST_PATH/private.key -out $TEST_PATH/request.csr -subj "/CN=${DontUseMe}" +openssl req -passin pass:dontusethiskey -new -key $TEST_PATH/private.key -out $TEST_PATH/request.csr -subj "/CN=${TEST_CN}" echo "[+] Generating test certificate " openssl x509 -req -passin pass:dontusethiskey -days 365 -in $TEST_PATH/request.csr -CA $TEST_PATH/ca.crt -CAkey $TEST_PATH/ca.key -set_serial 01 -out $TEST_PATH/test.crt echo "[+] Removing passphrase from test key" diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index b56c409..cc23071 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -226,7 +226,7 @@ public class JobServClient { // fails if port is improperly formatted or if an ssl exception occurs JobServClient client; try { - client = new JobServClient(args[0], Integer.parseInt(args[1]), args[2], args[1], args[0]); + client = new JobServClient(args[3], Integer.parseInt(args[4]), args[2], args[1], args[0]); } catch (NumberFormatException e) { System.out.println("Invalid Port"); @@ -239,7 +239,7 @@ public class JobServClient { // declare pid up here so that multiple switch cases can use it int candidatePid; // parse remaining args - switch (args[2]) { + switch (args[5]) { case "new": if (args.length < 7) { System.out.println("Improper formatting, try client --help"); diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index a3b6021..c4ae512 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -31,31 +31,30 @@ public class JobServServer { private Server server; private final int port; - private final String certChainFilePath; - private final String privateKeyFilePath; - private final String trustCertCollectionFilePath; + private final SslContext ssl; + /* + * Constructor + * Sets port and builds sslContext + */ public JobServServer(int port, - String certChainFilePath, - String privateKeyFilePath, - String trustCertCollectionFilePath) { + String serverCert, + String privateKey, + String trustStore) { this.port = port; - this.certChainFilePath = certChainFilePath; - this.privateKeyFilePath = privateKeyFilePath; - this.trustCertCollectionFilePath = trustCertCollectionFilePath; + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(serverCert), new File(privateKey)); + + // Mutual TLS trust store and require client auth + sslContextBuilder.trustManager(new File(trustStore)); + sslContextBuilder.clientAuth(ClientAuth.REQUIRE); + + this.ssl = GrpcSslContexts.configure(sslClientContextBuilder).build(); } - private SslContextBuilder getSslContextBuilder() { - SslContextBuilder sslClientContextBuilder = SslContextBuilder.forServer(new File(certChainFilePath), - new File(privateKeyFilePath)); - - sslClientContextBuilder.trustManager(new File(trustCertCollectionFilePath)); - sslClientContextBuilder.clientAuth(ClientAuth.REQUIRE); - - return GrpcSslContexts.configure(sslClientContextBuilder); - } - - // starts the GRPC API Server + /* + * start() + * this initializes the server + */ private void start() throws IOException { // TODO: this should be passed in from a configuration manager server = NettyServerBuilder.forPort(port) @@ -76,25 +75,38 @@ public class JobServServer { }); } + /* + * stop() + * This is called when ctrl+c is pressed + */ private void stop() { if (server != null) { server.shutdown(); } } - // spinlock for main() + /* + * blockUntilShutdown() + * This is more or less the main loop of the server. + * It spins until shutdown is called. + */ private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } - // Main function. starts GRPC server and spins until server is shutdown + /* + * main() + * Entrypoint of hte server + * parses args and initializes a server object. + * calls server main loop. + */ public static void main(String[] args) throws IOException, InterruptedException { int port = 8448; // TODO: port and key/cert files should be handled by a config manager if(args.length < 3) { - System.out.println("Usage: ./jobserv-server certchain privatekey truststore"); + System.out.println("Usage: ./jobserv-server cert privatekey truststore"); return; } From b74741427c83fd9c3e012a460d95d5f56deabdaa Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 13:26:15 -0700 Subject: [PATCH 20/63] refactor server side exception handling, better error messages in client --- src/main/java/JobServ/JobServClient.java | 10 +++++----- src/main/java/JobServ/JobServServer.java | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index cc23071..a330b1d 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -94,7 +94,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getOutput(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "[-] Request for output failed: %s", e.getStatus()); + logger.log(Level.WARNING, "(API Failure) Request for output failed: %s", e.getMessage()); return ""; } @@ -119,7 +119,7 @@ public class JobServClient { // blocking network operation response = blockingStub.newJob(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "[-] Request for new job failed!"); + logger.log(Level.WARNING, "(API Failure) Request for new job failed: %s", e.getMessage()); return -1; } @@ -147,7 +147,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getStatus(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "[-] Request for status failed!"); + logger.log(Level.WARNING, "(API Failure) Request for status failed: %s", e.getMessage()); return false; } @@ -172,7 +172,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getReturn(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "[-] Failed to get return code!"); + logger.log(Level.WARNING, "(API Failure) Failed to get return code: %s", e.getMessage()); return 278; } @@ -196,7 +196,7 @@ public class JobServClient { // blocking network operation response = blockingStub.killJob(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "[-] Failed to send request!"); + logger.log(Level.WARNING, "(API Failure) Failed to send request: %s", e.getMessage()); return; } diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index c4ae512..8af7845 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -13,9 +13,10 @@ import io.grpc.ServerBuilder; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; - +import javax.net.ssl.SSLException; import java.io.File; import java.io.IOException; import java.util.logging.Logger; @@ -40,7 +41,7 @@ public class JobServServer { public JobServServer(int port, String serverCert, String privateKey, - String trustStore) { + String trustStore) throws SSLException { this.port = port; SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(serverCert), new File(privateKey)); @@ -48,7 +49,7 @@ public class JobServServer { sslContextBuilder.trustManager(new File(trustStore)); sslContextBuilder.clientAuth(ClientAuth.REQUIRE); - this.ssl = GrpcSslContexts.configure(sslClientContextBuilder).build(); + this.ssl = GrpcSslContexts.configure(sslContextBuilder).build(); } /* @@ -59,7 +60,7 @@ public class JobServServer { // TODO: this should be passed in from a configuration manager server = NettyServerBuilder.forPort(port) .addService(new ShellServerService()) - .sslContext(getSslContextBuilder().build()) + .sslContext(this.ssl) .build() .start(); logger.info("Server initialized!"); @@ -110,7 +111,15 @@ public class JobServServer { return; } - final JobServServer server = new JobServServer(8448, args[0], args[1], args[2]); + JobServServer server; + + try{ + server = new JobServServer(8448, args[0], args[1], args[2]); + } catch (SSLException e) { + System.out.println(e.getMessage()); + return; + } + server.start(); server.blockUntilShutdown(); } From f63fe57b984a15f3d52cbe33eab3ddc44cfbcf38 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 13:55:41 -0700 Subject: [PATCH 21/63] build wrapper now extracts distributions of software and adds keys --- .gitignore | 3 +++ buildwrapper.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/.gitignore b/.gitignore index fecc8c1..baf6b85 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ build # Dont commit certs resources/* + +# Dont commit certs or compiled software +staging/* diff --git a/buildwrapper.sh b/buildwrapper.sh index 85132ac..360fe51 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -10,10 +10,12 @@ TEST_CA_CN=jobserv-bad-cert-ca TEST_CN=jobserv-bad-cert TEST_PATH=resources/test +# refactor this to test for directory existanc rm -rf resources/* mkdir resources/client mkdir resources/server mkdir resources/test +rm -rf staging # Get passwords for CAs @@ -78,3 +80,31 @@ cat $SERVER_PATH/ca.crt $CLIENT_PATH/ca.crt > resources/truststore.pem echo "[+] initiating gradle build" ./gradlew clean build + +# Ideally this next section would be done with gradle +# Unfortunately gradle's protobuf distribution plugin does not seem to have facilities to manually include certs +# Or to specify seperate client and server tarballs for that matter +# Definitely more research on gradle should be done, but after JobServ hits MVP +echo "[+] extracting built code" +mkdir staging +mkdir staging/client +mkdir staging/server +mkdir staging/test +tar -xvf build/distributions/JobServ.tar -C staging/client +tar -xvf build/distributions/JobServ.tar -C staging/server +tar -xvf build/distributions/JobServ.tar -C staging/test + +echo "[+] removing server capabilities from client" +rm staging/client/JobServ/bin/jobserv-server staging/client/JobServ/bin/jobserv-server.bat + +echo "[+] removing client capabilities from server" +rm staging/client/JobServ/bin/jobserv-client staging/client/JobServ/bin/jobserv-client.bat + +echo "[+] populating certificates" +cp resources/server/server.crt staging/server/ +cp resources/server/private.pem staging/server/ +cp resources/truststore.pem staging/server/ +cp resources/client/client.crt staging/client/ +cp resources/client/private.pem staging/client/ +cp resources/truststore.pem staging/client/ +cp -r resources/* staging/test/ From bfd5d400135a105a0a17d074cbe55edd5ad42990 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 14:16:09 -0700 Subject: [PATCH 22/63] better client error messages --- buildwrapper.sh | 2 +- src/main/java/JobServ/JobServClient.java | 10 +++++----- src/main/java/JobServ/JobServServer.java | 17 +++++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/buildwrapper.sh b/buildwrapper.sh index 360fe51..f0aeb5a 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -98,7 +98,7 @@ echo "[+] removing server capabilities from client" rm staging/client/JobServ/bin/jobserv-server staging/client/JobServ/bin/jobserv-server.bat echo "[+] removing client capabilities from server" -rm staging/client/JobServ/bin/jobserv-client staging/client/JobServ/bin/jobserv-client.bat +rm staging/server/JobServ/bin/jobserv-client staging/server/JobServ/bin/jobserv-client.bat echo "[+] populating certificates" cp resources/server/server.crt staging/server/ diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index a330b1d..b1839f5 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -94,7 +94,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getOutput(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for output failed: %s", e.getMessage()); + logger.log(Level.WARNING, "(API Failure) Request for output failed: " + e.getStatus()); return ""; } @@ -119,7 +119,7 @@ public class JobServClient { // blocking network operation response = blockingStub.newJob(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for new job failed: %s", e.getMessage()); + logger.log(Level.WARNING, "(API Failure) Request for new job failed: " + e.getStatus()); return -1; } @@ -147,7 +147,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getStatus(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for status failed: %s", e.getMessage()); + logger.log(Level.WARNING, "(API Failure) Request for status failed: " + e.getStatus()); return false; } @@ -172,7 +172,7 @@ public class JobServClient { // blocking network operation response = blockingStub.getReturn(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Failed to get return code: %s", e.getMessage()); + logger.log(Level.WARNING, "(API Failure) Failed to get return code: " + e.getStatus()); return 278; } @@ -196,7 +196,7 @@ public class JobServClient { // blocking network operation response = blockingStub.killJob(request); } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Failed to send request: %s", e.getMessage()); + logger.log(Level.WARNING, "(API Failure) Failed to send request: " + e.getStatus()); return; } diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 8af7845..6156403 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -17,6 +17,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import javax.net.ssl.SSLException; +import java.util.InputMismatchException; import java.io.File; import java.io.IOException; import java.util.logging.Logger; @@ -104,17 +105,21 @@ public class JobServServer { * calls server main loop. */ public static void main(String[] args) throws IOException, InterruptedException { - int port = 8448; // TODO: port and key/cert files should be handled by a config manager - - if(args.length < 3) { - System.out.println("Usage: ./jobserv-server cert privatekey truststore"); + // TODO: port and key/cert files should be handled by a config manager + if(args.length < 4) { + System.out.println("Usage: ./jobserv-server port cert privatekey truststore"); return; } JobServServer server; - try{ - server = new JobServServer(8448, args[0], args[1], args[2]); + try { + server = new JobServServer(Integer.parseInt(args[0]), args[1], args[2], args[3]); + + } catch (InputMismatchException e) { + System.out.println("Invalid port!"); + return; + } catch (SSLException e) { System.out.println(e.getMessage()); return; From f7776e72693b0189c64e6814f5544209ee8749b6 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 14:20:33 -0700 Subject: [PATCH 23/63] dont combine truststore --- buildwrapper.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/buildwrapper.sh b/buildwrapper.sh index f0aeb5a..1ca1b02 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -75,9 +75,6 @@ openssl pkcs8 -topk8 -nocrypt -in $CLIENT_PATH/private.key -out $CLIENT_PATH/pri openssl pkcs8 -topk8 -nocrypt -in $SERVER_PATH/private.key -out $SERVER_PATH/private.pem openssl pkcs8 -topk8 -nocrypt -in $TEST_PATH/private.key -out $TEST_PATH/private.pem -echo "[+] creating combine trust store" -cat $SERVER_PATH/ca.crt $CLIENT_PATH/ca.crt > resources/truststore.pem - echo "[+] initiating gradle build" ./gradlew clean build @@ -103,8 +100,8 @@ rm staging/server/JobServ/bin/jobserv-client staging/server/JobServ/bin/jobserv- echo "[+] populating certificates" cp resources/server/server.crt staging/server/ cp resources/server/private.pem staging/server/ -cp resources/truststore.pem staging/server/ +cp resources/client/ca.crt staging/server/ cp resources/client/client.crt staging/client/ cp resources/client/private.pem staging/client/ -cp resources/truststore.pem staging/client/ +cp resources/server/ca.crt staging/client/ cp -r resources/* staging/test/ From 8982353f0adba65f2fefb9b50824fc08e34f6436 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 14:25:25 -0700 Subject: [PATCH 24/63] CNs cant be arbitrary --- buildwrapper.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/buildwrapper.sh b/buildwrapper.sh index 1ca1b02..9d6a19f 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -1,13 +1,14 @@ #!/bin/sh +read -p "Enter Server CN (localhost or address): " SRVNAME +read -p "Enter Client CN (localhost or address): " CLTNAME + SERVER_CA_CN=jobserv-server-ca -SERVER_CN=jobserv-server SERVER_PATH=resources/server CLIENT_CA_CN=jobserv-client-ca -CLIENT_CN=jobserv-client CLIENT_PATH=resources/client TEST_CA_CN=jobserv-bad-cert-ca -TEST_CN=jobserv-bad-cert +TEST_CN=localhost TEST_PATH=resources/test # refactor this to test for directory existanc @@ -43,7 +44,7 @@ openssl req -passin pass:dontusethiskey -new -x509 -days 365 -key $TEST_PATH/ca. echo "[+] Generating Server key" openssl genrsa -passout pass:${SRVCAPASS} -aes256 -out $SERVER_PATH/private.key 4096 echo "[+] Generating Server signing request" -openssl req -passin pass:${SRVCAPASS} -new -key $SERVER_PATH/private.key -out $SERVER_PATH/request.csr -subj "/CN=${SERVER_CN}" +openssl req -passin pass:${SRVCAPASS} -new -key $SERVER_PATH/private.key -out $SERVER_PATH/request.csr -subj "/CN=${SRVNAME}" echo "[+] Generating Server certificate " openssl x509 -req -passin pass:${SRVCAPASS} -days 365 -in $SERVER_PATH/request.csr -CA $SERVER_PATH/ca.crt -CAkey $SERVER_PATH/ca.key -set_serial 01 -out $SERVER_PATH/server.crt echo "[+] Removing passphrase from server key" @@ -53,7 +54,7 @@ openssl rsa -passin pass:${SRVCAPASS} -in $SERVER_PATH/private.key -out $SERVER_ echo "[+] Generating Client key" openssl genrsa -passout pass:${CLTCAPASS} -aes256 -out $CLIENT_PATH/private.key 4096 echo "[+] Generating Client signing request" -openssl req -passin pass:${CLTCAPASS} -new -key $CLIENT_PATH/private.key -out $CLIENT_PATH/request.csr -subj "/CN=${CLIENT_CN}" +openssl req -passin pass:${CLTCAPASS} -new -key $CLIENT_PATH/private.key -out $CLIENT_PATH/request.csr -subj "/CN=${CLTNAME}" echo "[+] Generating Client certificate " openssl x509 -req -passin pass:${CLTCAPASS} -days 365 -in $CLIENT_PATH/request.csr -CA $CLIENT_PATH/ca.crt -CAkey $CLIENT_PATH/ca.key -set_serial 01 -out $CLIENT_PATH/client.crt echo "[+] Removing passphrase from client key" From 75c3dcda61c9edc0709bf6779d34af3601e5d8eb Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 19 May 2019 14:39:41 -0700 Subject: [PATCH 25/63] updated README.md --- README.md | 30 +++++++++++++++++------- src/main/java/JobServ/JobServClient.java | 8 ++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 053c311..e778875 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,33 @@ Remote Procedure Calls over the protobuf API # Requirements - openssl +- tar # Building Gradle will manage dependencies, generate code, compile the java, and package the code. Simply run the folllowing command: ```shell -$ gradle clean build +$ ./buildwrapper.sh ``` -Gradle will package both the client and server into both a zip and a tarball. -The tarball is located in build/distributions. After extracting one of the archives on a target machine the following commands will run JobServ. -Make sure you are in the directory you extracted the archive to. -```shell -$ ./bin/jobserv-server -$ ./bin/jobserv-client (host) (port) +buildwrapper will ask you for details about the client and server, if you are testing this software both CNs can be set to localhost. +buildwrapper will then generate CAs for the Client and Server, and signed certs for the Client and Server. IN addition a seperate, third CA and cert will be generated for testing purposes. +Gradle will then generate protobuf source and compile it with the source for the client and server. +After gradle is completed buildwrapper will organize the sources with their respective certs in the staging folder. In addition to a server folder and a client folder, there will be a test folder which has a copy of all certs and both server and client functionality. The test CA is not trusted by the server or the client by default. As such, the test cert can be used to induce a mutual tls authentication failure. + +# Running +After build, the programs can be found in the staging folder. +After changing directory to the 'staging/client' folder or the 'staging/server' folder, either program can be run as follows: + +``` +$ ./JobServ/bin/jobserv-server (port) server.crt private.pem ca.crt +$ ./JobServ/bin/jobserv-client private.pem client.crt ca.crt (hostname) (port) (command) (arguments) +``` +alternatively, for guidance: + +``` +$ ./JobServ/bin/jobserv-server +$ ./JobServ/bin/jobserv-client help ``` -(TODO: seperate archives for client and server) # Testing -(TODO: tests for mtls, job control module) +(TODO: job control module) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index b1839f5..7ec17f6 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -211,14 +211,12 @@ public class JobServClient { * Parses arguments and calls the correct function */ public static void main(String[] args) throws Exception { - if (args.length == 1 && args[0] == "help"){ - outputHelp(); - } // check args if (args.length < 7) { System.out.println("Usage: $ ./jobserv-client privatekey, cert, truststore, host, port, command, args"); System.out.println("Or try $ ./jobserv-client help"); + outputHelp(); return; } @@ -342,6 +340,10 @@ public class JobServClient { } } + /* + * outputHelp() + * writes help information about all commands in the shell to screen + */ public static void outputHelp() { System.out.println("... new (command)"); System.out.println("Starts a new process on the server"); From 5724953e9d6fd86a425dda61cc94dbfa1e2da0b6 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 01:37:41 -0700 Subject: [PATCH 26/63] refactored out re-entrant code in client --- src/main/java/JobServ/JobServClient.java | 90 +++++++++++------------- src/main/java/JobServ/JobServServer.java | 2 - 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 7ec17f6..be67d06 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -220,15 +220,16 @@ public class JobServClient { return; } - // start client - // fails if port is improperly formatted or if an ssl exception occurs JobServClient client; try { client = new JobServClient(args[3], Integer.parseInt(args[4]), args[2], args[1], args[0]); + // Likely bad port } catch (NumberFormatException e) { System.out.println("Invalid Port"); return; + + // bad cert or key format } catch (SSLException e) { System.out.println(e.getMessage()); return; @@ -254,71 +255,40 @@ public class JobServClient { break; case "output": - if (args.length != 7) { - System.out.println("Improper formatting, try client --help"); - break; - } - - try { - candidatePid = Integer.parseInt(args[6]); - } catch (InputMismatchException e) { - System.out.println(args[6] + " is not a valid int, much less a valid pid"); - break; - } + candidatePid = getPidArg(args, 7); + if (candidatePid < 0) { + break; + } String processOutput = client.getProcessOutput(candidatePid); System.out.println(processOutput); break; case "status": - if (args.length != 7) { - System.out.println("Improper formatting, try client --help"); - break; - } - - try { - candidatePid = Integer.parseInt(args[6]); - - } catch (InputMismatchException e) { - System.out.println(args[6] + " is not a valid int, much less a valid pid"); - break; - } + candidatePid = getPidArg(args, 7); + if (candidatePid < 0) { + break; + } Boolean processStatus = client.getProcessStatus(candidatePid); System.out.printf("Process is currently running? %b\n", processStatus); break; case "kill": - if (args.length != 7) { - System.out.println("Improper formatting, try client --help"); - break; - } - - try { - candidatePid = Integer.parseInt(args[6]); - - } catch (InputMismatchException e) { - System.out.println(args[6] + " is not a valid int, much less a valid pid"); - break; - } + candidatePid = getPidArg(args, 7); + if (candidatePid < 0) { + break; + } client.killProcess(candidatePid); System.out.println("End process request recieved!"); break; case "return": - if (args.length != 7) { - System.out.println("Improper formatting, try client --help"); - break; - } - - try { - candidatePid = Integer.parseInt(args[6]); - - } catch (InputMismatchException e) { - System.out.println(args[6] + " is not a valid int, much less a valid pid"); - break; - } + candidatePid = getPidArg(args, 7); + if (candidatePid < 0) { + break; + } int returnCode = client.getProcessReturn(candidatePid); @@ -340,6 +310,28 @@ public class JobServClient { } } + /* + * getPidArg() + * reentrant code was found in all commands except newjob + * this function pulls the pid argument and wraps around the integer case + * returns -1 (an invalid PID) if bad index or unparsable int + */ + private static int getPidArg(String[] args, int index) { + if (args.length < index) { + System.out.println("Improper formatting, try client --help"); + return -1; + } + + try { + return Integer.parseInt(args[6]); + + } catch (InputMismatchException e) { + System.out.println(args[6] + " is not a valid int, much less a valid pid"); + return -1; + } + + } + /* * outputHelp() * writes help information about all commands in the shell to screen diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 6156403..483e865 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -69,8 +69,6 @@ public class JobServServer { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { - // JVM shutdown might break logger functionality - // so investigate this.... logger.info("Shutting down server"); JobServServer.this.stop(); } From 77e0cb3450fbfda282851f43d8e2b8e60b62e7da Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 13:25:50 -0700 Subject: [PATCH 27/63] added tests for TLS Authentication --- src/main/java/JobServ/JobServClient.java | 4 +- .../JobServ/JobServerAuthenticationTest.java | 182 ++++++++++++++++++ .../JobServ/JobServerAuthenticationTest.java~ | 27 +++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/java/JobServ/JobServerAuthenticationTest.java create mode 100644 src/test/java/JobServ/JobServerAuthenticationTest.java~ diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index be67d06..cb7751e 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -105,6 +105,8 @@ public class JobServClient { * sendNewJobMessage() * sends a shell command to the api server * returns new pid of job + * or -1 if server failed to create job + * or -2 if failed to connect to API */ public int sendNewJobMessage(String command) { // thought of escaping this, but the vulnerability is only client side, from client user input. @@ -120,7 +122,7 @@ public class JobServClient { response = blockingStub.newJob(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "(API Failure) Request for new job failed: " + e.getStatus()); - return -1; + return -2; } if(response.getPid() == -1) { diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java new file mode 100644 index 0000000..1388899 --- /dev/null +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -0,0 +1,182 @@ +/* + * JobServerAuthenticationTest + * + * v1.0 + * + * May 21, 2019 + */ + +package JobServ; + +import static org.junit.Assert.assertEquals; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.netty.GrpcSslContexts; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +/* + * JobServerAuthenticationTest + * Creates a client using authorized certs and another one using unauthorized certs + * Ensures only the client with authorized certs can connect to the server. + * For more information on the hardcoded paths check buildwrapper.sh + */ + +@RunWith(JUnit4.class) +public class JobServerAuthorizationTest { + + // Authorized client key/cert/ca + private final String clientCa = "resources/client/ca.crt"; + private final String clientKey = "resources/client/private.pem"; + private final String clientCert = "resources/client/client.crt"; + + // Authorized server key/cert/ca + private final String serverCa = "resources/server/ca.crt"; + private final String serverKey = "resources/server/private.pem"; + private final String serverCert = "resources/server/server.crt"; + + // controlled failure key/cert/ca + private final String badCa = "resources/test/ca.crt"; + private final String badKey = "resources/test/private.pem"; + private final String badCert = "resources/test/test.crt"; + + // Automates (graceful) shutdown at end of tests + @Rule + public final GrpcCleanupRule grpcCleanup = newCleanupRule(); + + private final ShellServerGrpc.ShellServerImplBase = mock(ShellServerGrpc.ShellServerImplBase.class, + delegatesTo(new ShellServerGrpc.ShellServerImplBase() {})); + + // badClient uses unauthorized certs + private JobServClient goodClient; + private JobServClient badClient; + + // was setUp able to use SSL Certs + private Boolean serverSslInitialized; + private Boolean clientSslInitialized; + + /* + * setUp() + * generates both clients and the server + */ + @Before + public void setUp() throws Exception { + String serverName = InProcessServerBuilder.generateName(); + + try { + // generate SSL contexts + SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), + new File(serverKey)); + serverContextBuilder.trustManager(new File(clientCa)); + serverContextBuilder.clientAuth(ClientAuth.REQUIRE); + + grpcCleanup.register(InProcessServerBuilder.forName(serverName) + .sslContext(serverContextBuilder.build()) + .directExecutor() + .addService(this.serviceImpl); + .build().start()); + + this.serverSslInitialized = true; + + } catch (SSLException e) { + // One of the certs or keys was bad, ssl cannot be used + this.serverSslInitialized = false; + grpcCleanup.register(InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(this.serviceImpl).build().start()); + } + + // generate ssl for clients + if (this.serverSslInitialized) { + try { + SslContextBuilder goodClientBuilder = SslContextBuilder.forClient(); + goodClientBuilder.trustManager(new File(serverCa)); + goodClientBuilder.keyManager(new File(clientCert), new File(clientKey)); + + SslContextBuilder badClientBuilder = SslContextBuilder.forClient(); + badClientBuilder.trustManager(new File(serverCa)); + badClientBuilder.keyManager(new File(badCert), new File(badKey)); + + ManagedChannel goodChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) + .sslContext(goodClientBuilder.build()) + .directExecutor() + .build()); + + ManagedChannel badChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) + .sslContext(badClientBuilder.build()) + .directExecutor() + .build()); + + goodClient = new JobServClient(goodChannel); + badClient = new JobServClient(badChannel); + this.clientSslInitialized = true; + + } catch (SSLException e) { + this.clientSslInitialized = false; + ManagedChannel nonSslChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) + .directExecutor() + .build()); + goodClient = new JobServClient(nonSslChannel); + badClient = null; // + } + } + } + + /* + * Server TLS Test + * fails if server SslContext generation threw an SSLException + */ + @Test + public void serverTlsTest() { + assertEquals(this.serverSslInitialized, true); + } + + /* + * Client TLS Test + * fails if client SslContext generation threw an SSL Exception + */ + @Test + public void clientTlsTest() { + assertEquals(this.clientSslInitialized, true); + } + + /* + * TLS Cert Auth Negative Test + * fails if badClient can successfully make requests of the server + */ + @Test + public void certAuthNegTest() { + assertEquals(clientSslInitialized, true); + int result = badClient.sendNewJobMessage("test command"); + assertEquals(result, -2); + } + + /* + * TLS Cert Auth Positive Test + * fails if goodClient cannot make requests of the server + */ + @Test + public void certAuthPosTest() { + assertEquals(clientSslInitialized, true); + int result = goodClient.sendNewJobMessage("test command"); + assertNotEquals(result, -2); + } +} diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java~ b/src/test/java/JobServ/JobServerAuthenticationTest.java~ new file mode 100644 index 0000000..c289791 --- /dev/null +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java~ @@ -0,0 +1,27 @@ +/* + * JobServerAuthenticationTest + * + * v1.0 + * + * May 21, 2019 + */ + +package JobServ; + +import static org.junit.Assert.assertEquals; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; \ No newline at end of file From 87681bc0e45939d8a8160e5760b212b8e8c6bd2a Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 13:26:49 -0700 Subject: [PATCH 28/63] removed accidental vim swaps --- .gitignore | 3 +++ .../JobServ/JobServerAuthenticationTest.java~ | 27 ------------------- 2 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 src/test/java/JobServ/JobServerAuthenticationTest.java~ diff --git a/.gitignore b/.gitignore index baf6b85..e441be8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ build \#* .\#* +# Ignore vim swapfiles +*\~ + # Dont commit certs resources/* diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java~ b/src/test/java/JobServ/JobServerAuthenticationTest.java~ deleted file mode 100644 index c289791..0000000 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java~ +++ /dev/null @@ -1,27 +0,0 @@ -/* - * JobServerAuthenticationTest - * - * v1.0 - * - * May 21, 2019 - */ - -package JobServ; - -import static org.junit.Assert.assertEquals; -import static org.mockito.AdditionalAnswers.delegatesTo; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import io.grpc.ManagedChannel; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; -import io.grpc.stub.StreamObserver; -import io.grpc.testing.GrpcCleanupRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; \ No newline at end of file From c599902ad590b4eba4ed2b056713f5d87ffd74e5 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 13:56:37 -0700 Subject: [PATCH 29/63] refactored client constructor to support class dependancy injection --- build.gradle | 4 ++- src/main/java/JobServ/JobServClient.java | 29 ++++++++----------- .../JobServ/JobServerAuthenticationTest.java | 14 +++++---- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index e3d3779..7dae974 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,9 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-jre' // Use JUnit test framework - testImplementation 'junit:junit:4.12' + testImplementation "io.grpc:grpc-testing:${grpcVersion}" + testImplementation "junit:junit:4.12" + testImplementation "org.mockito:mockito-core:2.25.1" // Used by GRPC generated code compile 'org.glassfish:javax.annotation:10.0-b28' diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index cb7751e..344b0ae 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -48,23 +48,10 @@ public class JobServClient { /* * Constructor - * Creates an SslContext from cert, key, and trust store - * Creates a ManagedChannel object from SSL Parameters * Spawns a new blockingStub for network operations with the server */ - public JobServClient(String host, - int port, - String trustStore, - String clientCert, - String clientPrivateKey) throws SSLException { - SslContextBuilder builder = GrpcSslContexts.forClient(); - builder.trustManager(new File(trustStore)); - builder.keyManager(new File(clientCert), new File(clientPrivateKey)); - - this.channel = NettyChannelBuilder.forAddress(host, port) - .sslContext(builder.build()) - .build(); - + public JobServClient(ManagedChannel channel) { + this.channel = channel; blockingStub = ShellServerGrpc.newBlockingStub(this.channel); } @@ -210,7 +197,7 @@ public class JobServClient { /* * main() * Client entrypoint - * Parses arguments and calls the correct function + * Parses arguments, initializes client, and calls the correct functions */ public static void main(String[] args) throws Exception { @@ -224,7 +211,15 @@ public class JobServClient { JobServClient client; try { - client = new JobServClient(args[3], Integer.parseInt(args[4]), args[2], args[1], args[0]); + SslContextBuilder builder = GrpcSslContexts.forClient(); + builder.trustManager(new File(args[2])); + builder.keyManager(new File(args[1]), new File(args[0])); + + ManagedChannel channel = NettyChannelBuilder.forAddress(args[3], Integer.parseInt(args[4])) + .sslContext(builder.build()) + .build(); + + client = new JobServClient(channel); // Likely bad port } catch (NumberFormatException e) { diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index 1388899..2173801 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -8,6 +8,9 @@ package JobServ; +import java.io.File; +import javax.net.ssl.SSLException; + import static org.junit.Assert.assertEquals; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.Mockito.mock; @@ -41,7 +44,7 @@ import org.mockito.ArgumentMatchers; */ @RunWith(JUnit4.class) -public class JobServerAuthorizationTest { +public class JobServerAuthenticationTest { // Authorized client key/cert/ca private final String clientCa = "resources/client/ca.crt"; @@ -60,9 +63,9 @@ public class JobServerAuthorizationTest { // Automates (graceful) shutdown at end of tests @Rule - public final GrpcCleanupRule grpcCleanup = newCleanupRule(); + public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - private final ShellServerGrpc.ShellServerImplBase = mock(ShellServerGrpc.ShellServerImplBase.class, + private final ShellServerGrpc.ShellServerImplBase serviceImpl= mock(ShellServerGrpc.ShellServerImplBase.class, delegatesTo(new ShellServerGrpc.ShellServerImplBase() {})); // badClient uses unauthorized certs @@ -91,7 +94,7 @@ public class JobServerAuthorizationTest { grpcCleanup.register(InProcessServerBuilder.forName(serverName) .sslContext(serverContextBuilder.build()) .directExecutor() - .addService(this.serviceImpl); + .addService(this.serviceImpl) .build().start()); this.serverSslInitialized = true; @@ -177,6 +180,7 @@ public class JobServerAuthorizationTest { public void certAuthPosTest() { assertEquals(clientSslInitialized, true); int result = goodClient.sendNewJobMessage("test command"); - assertNotEquals(result, -2); + Boolean assertCondition = result == -2; + assertEquals(result, false); } } From ea3f84b830f38c7c3f005c69853a4c951ee868bf Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 15:04:18 -0700 Subject: [PATCH 30/63] refactored server for dependancy injection, combined tests to run correctly in a multithreaded build --- build.gradle | 22 ++- src/main/java/JobServ/JobServServer.java | 44 +++--- .../JobServ/JobServerAuthenticationTest.java | 136 +++++++----------- 3 files changed, 95 insertions(+), 107 deletions(-) diff --git a/build.gradle b/build.gradle index 7dae974..ba5b8d2 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ plugins { id 'java' id 'com.google.protobuf' version '0.8.8' id 'application' + id 'com.adarshr.test-logger' version '1.6.0' } def grpcVersion = '1.20.0' @@ -49,6 +50,25 @@ dependencies { compile 'io.netty:netty-tcnative-boringssl-static:2.0.22.Final' } +test { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' +} + +testlogger { + theme 'standard' + showExceptions true + slowThreshold 2000 + showSummary true + showPassed true + showSkipped true + showFailed true + showStandardStreams false + showPassedStandardStreams true + showSkippedStandardStreams true + showFailedStandardStreams true +} + // Define the main class for the application mainClassName = 'JobServ.JobServClient' @@ -88,4 +108,4 @@ applicationDistribution.into('bin') { from(Server) from(Client) fileMode = 0755 -} \ No newline at end of file +} diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 483e865..07e41fa 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -32,25 +32,17 @@ public class JobServServer { private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); private Server server; - private final int port; - private final SslContext ssl; /* * Constructor - * Sets port and builds sslContext + * builds server object */ - public JobServServer(int port, - String serverCert, - String privateKey, - String trustStore) throws SSLException { - this.port = port; - SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(serverCert), new File(privateKey)); - - // Mutual TLS trust store and require client auth - sslContextBuilder.trustManager(new File(trustStore)); - sslContextBuilder.clientAuth(ClientAuth.REQUIRE); - - this.ssl = GrpcSslContexts.configure(sslContextBuilder).build(); + public JobServServer(SslContext ssl, int port) throws IOException { + this.server = NettyServerBuilder.forPort(port) + .addService(new ShellServerService()) + .sslContext(ssl) + .build() + .start(); } /* @@ -59,11 +51,7 @@ public class JobServServer { */ private void start() throws IOException { // TODO: this should be passed in from a configuration manager - server = NettyServerBuilder.forPort(port) - .addService(new ShellServerService()) - .sslContext(this.ssl) - .build() - .start(); + server.start(); logger.info("Server initialized!"); Runtime.getRuntime().addShutdownHook(new Thread() { @@ -112,7 +100,14 @@ public class JobServServer { JobServServer server; try { - server = new JobServServer(Integer.parseInt(args[0]), args[1], args[2], args[3]); + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(args[1]), new File(args[2])); + + // Mutual TLS trust store and require client auth + sslContextBuilder.trustManager(new File(args[3])); + sslContextBuilder.clientAuth(ClientAuth.REQUIRE); + + server = new JobServServer(GrpcSslContexts.configure(sslContextBuilder).build(), + Integer.parseInt(args[0])); } catch (InputMismatchException e) { System.out.println("Invalid port!"); @@ -121,9 +116,12 @@ public class JobServServer { } catch (SSLException e) { System.out.println(e.getMessage()); return; - } + + } catch (IOException e) { + System.out.println(e.getMessage()); + return; + } - server.start(); server.blockUntilShutdown(); } } diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index 2173801..e332cfe 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -10,6 +10,7 @@ package JobServ; import java.io.File; import javax.net.ssl.SSLException; +import java.io.IOException; import static org.junit.Assert.assertEquals; import static org.mockito.AdditionalAnswers.delegatesTo; @@ -17,8 +18,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import io.grpc.ManagedChannel; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.StatusRuntimeException; +import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.netty.GrpcSslContexts; @@ -28,7 +30,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,44 +48,40 @@ import org.mockito.ArgumentMatchers; @RunWith(JUnit4.class) public class JobServerAuthenticationTest { + private final String projectRoot = ""; + // Authorized client key/cert/ca - private final String clientCa = "resources/client/ca.crt"; - private final String clientKey = "resources/client/private.pem"; - private final String clientCert = "resources/client/client.crt"; + private final String clientCa = projectRoot + "resources/client/ca.crt"; + private final String clientKey = projectRoot + "resources/client/private.pem"; + private final String clientCert = projectRoot + "resources/client/client.crt"; // Authorized server key/cert/ca - private final String serverCa = "resources/server/ca.crt"; - private final String serverKey = "resources/server/private.pem"; - private final String serverCert = "resources/server/server.crt"; + private final String serverCa = projectRoot + "resources/server/ca.crt"; + private final String serverKey = projectRoot + "resources/server/private.pem"; + private final String serverCert = projectRoot + "resources/server/server.crt"; // controlled failure key/cert/ca - private final String badCa = "resources/test/ca.crt"; - private final String badKey = "resources/test/private.pem"; - private final String badCert = "resources/test/test.crt"; - - // Automates (graceful) shutdown at end of tests - @Rule - public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - - private final ShellServerGrpc.ShellServerImplBase serviceImpl= mock(ShellServerGrpc.ShellServerImplBase.class, - delegatesTo(new ShellServerGrpc.ShellServerImplBase() {})); + private final String badCa = projectRoot + "resources/test/ca.crt"; + private final String badKey = projectRoot + "resources/test/private.pem"; + private final String badCert = projectRoot + "resources/test/test.crt"; // badClient uses unauthorized certs private JobServClient goodClient; private JobServClient badClient; + private JobServServer server; // was setUp able to use SSL Certs - private Boolean serverSslInitialized; - private Boolean clientSslInitialized; + private Boolean serverSslInitialized = true; + private Boolean clientSslInitialized = true; /* - * setUp() + * test constructor * generates both clients and the server */ - @Before - public void setUp() throws Exception { - String serverName = InProcessServerBuilder.generateName(); + public JobServerAuthenticationTest() throws Exception { + System.out.println("Ctrl print"); + try { // generate SSL contexts SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), @@ -91,42 +89,38 @@ public class JobServerAuthenticationTest { serverContextBuilder.trustManager(new File(clientCa)); serverContextBuilder.clientAuth(ClientAuth.REQUIRE); - grpcCleanup.register(InProcessServerBuilder.forName(serverName) - .sslContext(serverContextBuilder.build()) - .directExecutor() - .addService(this.serviceImpl) - .build().start()); - + this.server = new JobServServer(GrpcSslContexts.configure(serverContextBuilder).build(), 8448); this.serverSslInitialized = true; } catch (SSLException e) { - // One of the certs or keys was bad, ssl cannot be used this.serverSslInitialized = false; - grpcCleanup.register(InProcessServerBuilder.forName(serverName) - .directExecutor() - .addService(this.serviceImpl).build().start()); + System.err.println(e.getMessage()); + + } catch (IOException e) { + this.serverSslInitialized = false; + System.err.println(e.getMessage()); } // generate ssl for clients if (this.serverSslInitialized) { try { - SslContextBuilder goodClientBuilder = SslContextBuilder.forClient(); + SslContextBuilder goodClientBuilder = GrpcSslContexts.forClient(); goodClientBuilder.trustManager(new File(serverCa)); goodClientBuilder.keyManager(new File(clientCert), new File(clientKey)); - SslContextBuilder badClientBuilder = SslContextBuilder.forClient(); + SslContextBuilder badClientBuilder = GrpcSslContexts.forClient(); badClientBuilder.trustManager(new File(serverCa)); badClientBuilder.keyManager(new File(badCert), new File(badKey)); - ManagedChannel goodChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) - .sslContext(goodClientBuilder.build()) - .directExecutor() - .build()); + ManagedChannel goodChannel = NettyChannelBuilder.forAddress("localhost", 8448) + .sslContext(goodClientBuilder.build()) + .directExecutor() + .build(); - ManagedChannel badChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) - .sslContext(badClientBuilder.build()) - .directExecutor() - .build()); + ManagedChannel badChannel = NettyChannelBuilder.forAddress("localhost", 8448) + .sslContext(badClientBuilder.build()) + .directExecutor() + .build(); goodClient = new JobServClient(goodChannel); badClient = new JobServClient(badChannel); @@ -134,53 +128,29 @@ public class JobServerAuthenticationTest { } catch (SSLException e) { this.clientSslInitialized = false; - ManagedChannel nonSslChannel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName) - .directExecutor() - .build()); - goodClient = new JobServClient(nonSslChannel); - badClient = null; // + System.err.println(e.getMessage()); } + + } else { + this.clientSslInitialized = false; } } /* - * Server TLS Test - * fails if server SslContext generation threw an SSLException + * TLS Cert Auth Test + * this needed to be one test because running multiple tests at the same time + * fails as the server tries to rebind to the same port. */ @Test - public void serverTlsTest() { - assertEquals(this.serverSslInitialized, true); - } - - /* - * Client TLS Test - * fails if client SslContext generation threw an SSL Exception - */ - @Test - public void clientTlsTest() { - assertEquals(this.clientSslInitialized, true); - } - - /* - * TLS Cert Auth Negative Test - * fails if badClient can successfully make requests of the server - */ - @Test - public void certAuthNegTest() { - assertEquals(clientSslInitialized, true); + public void certificateAuthenticationTest() { + assertEquals(true, serverSslInitialized); + assertEquals(true, clientSslInitialized); + int result = badClient.sendNewJobMessage("test command"); - assertEquals(result, -2); - } + assertEquals(-2, result); - /* - * TLS Cert Auth Positive Test - * fails if goodClient cannot make requests of the server - */ - @Test - public void certAuthPosTest() { - assertEquals(clientSslInitialized, true); - int result = goodClient.sendNewJobMessage("test command"); + result = goodClient.sendNewJobMessage("test command"); Boolean assertCondition = result == -2; - assertEquals(result, false); + assertEquals(assertCondition, false); } } From 8f0ee527d4b07b6834efa7ecb37ab887133ccf98 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 15:05:45 -0700 Subject: [PATCH 31/63] readme additions for cert auth test --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e778875..680a1e8 100644 --- a/README.md +++ b/README.md @@ -32,4 +32,6 @@ $ ./JobServ/bin/jobserv-client help ``` # Testing -(TODO: job control module) +Running the gradle test task, or the buildwrapper will run all junit tests. +Currently that includes a test of certificate based authentication (Mutual TLS) +(TODO: job control module tests) From d8bcc82db100e81f3d0b59ce3e2ba6ef53e9bda3 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Tue, 21 May 2019 15:06:11 -0700 Subject: [PATCH 32/63] removed a debug print --- src/test/java/JobServ/JobServerAuthenticationTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index e332cfe..71a5db5 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -79,8 +79,6 @@ public class JobServerAuthenticationTest { * generates both clients and the server */ public JobServerAuthenticationTest() throws Exception { - - System.out.println("Ctrl print"); try { // generate SSL contexts From 37cd129b8ba1e9d695c7aba2d8060cf3fd12db2c Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 22 May 2019 01:01:12 -0700 Subject: [PATCH 33/63] oops! wrong argument --- src/main/java/JobServ/JobServClient.java | 10 +++++----- .../java/JobServ/JobServerAuthenticationTest.java | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 344b0ae..94a69a6 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -237,7 +237,7 @@ public class JobServClient { // parse remaining args switch (args[5]) { case "new": - if (args.length < 7) { + if (args.length < 6) { System.out.println("Improper formatting, try client --help"); break; } @@ -252,7 +252,7 @@ public class JobServClient { break; case "output": - candidatePid = getPidArg(args, 7); + candidatePid = getPidArg(args, 6); if (candidatePid < 0) { break; } @@ -262,7 +262,7 @@ public class JobServClient { break; case "status": - candidatePid = getPidArg(args, 7); + candidatePid = getPidArg(args, 6); if (candidatePid < 0) { break; } @@ -272,7 +272,7 @@ public class JobServClient { break; case "kill": - candidatePid = getPidArg(args, 7); + candidatePid = getPidArg(args, 6); if (candidatePid < 0) { break; } @@ -282,7 +282,7 @@ public class JobServClient { break; case "return": - candidatePid = getPidArg(args, 7); + candidatePid = getPidArg(args, 6); if (candidatePid < 0) { break; } diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index 71a5db5..2bec0a9 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -81,14 +81,14 @@ public class JobServerAuthenticationTest { public JobServerAuthenticationTest() throws Exception { try { - // generate SSL contexts - SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), + // generate SSL contexts + SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), new File(serverKey)); - serverContextBuilder.trustManager(new File(clientCa)); - serverContextBuilder.clientAuth(ClientAuth.REQUIRE); + serverContextBuilder.trustManager(new File(clientCa)); + serverContextBuilder.clientAuth(ClientAuth.REQUIRE); - this.server = new JobServServer(GrpcSslContexts.configure(serverContextBuilder).build(), 8448); - this.serverSslInitialized = true; + this.server = new JobServServer(GrpcSslContexts.configure(serverContextBuilder).build(), 8448); + this.serverSslInitialized = true; } catch (SSLException e) { this.serverSslInitialized = false; From 622da2d238f1a9528ecd39f33676d8e6fc9a698b Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 22 May 2019 16:23:33 -0700 Subject: [PATCH 34/63] Proces Controller Object --- src/main/java/JobServ/JobServClient.java | 61 ++++++----- src/main/java/JobServ/ProcessController.java | 101 +++++++++++++++++++ 2 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 src/main/java/JobServ/ProcessController.java diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 94a69a6..f5e5606 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -51,7 +51,7 @@ public class JobServClient { * Spawns a new blockingStub for network operations with the server */ public JobServClient(ManagedChannel channel) { - this.channel = channel; + this.channel = channel; blockingStub = ShellServerGrpc.newBlockingStub(this.channel); } @@ -146,8 +146,8 @@ public class JobServClient { /* * sends PID to server * returns process exit code - * returns a 0-255 return code or 277 if still running - * or 278 if error in API + * returns a 0-255 return code or 256 if still running + * or 257 if error in API */ public int getProcessReturn(int pid) { logger.info("[+] Requesting return code of a job"); @@ -162,7 +162,7 @@ public class JobServClient { response = blockingStub.getReturn(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "(API Failure) Failed to get return code: " + e.getStatus()); - return 278; + return 257; } return response.getProcessReturnCode(); @@ -211,14 +211,13 @@ public class JobServClient { JobServClient client; try { - SslContextBuilder builder = GrpcSslContexts.forClient(); - builder.trustManager(new File(args[2])); - builder.keyManager(new File(args[1]), new File(args[0])); + SslContextBuilder builder = GrpcSslContexts.forClient(); + builder.trustManager(new File(args[2])); + builder.keyManager(new File(args[1]), new File(args[0])); - ManagedChannel channel = NettyChannelBuilder.forAddress(args[3], Integer.parseInt(args[4])) - .sslContext(builder.build()) - .build(); - + ManagedChannel channel = NettyChannelBuilder.forAddress(args[3], Integer.parseInt(args[4])) + .sslContext(builder.build()) + .build(); client = new JobServClient(channel); // Likely bad port @@ -226,7 +225,7 @@ public class JobServClient { System.out.println("Invalid Port"); return; - // bad cert or key format + // bad cert or key format } catch (SSLException e) { System.out.println(e.getMessage()); return; @@ -253,19 +252,19 @@ public class JobServClient { case "output": candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } + if (candidatePid < 0) { + break; + } String processOutput = client.getProcessOutput(candidatePid); System.out.println(processOutput); break; - + case "status": candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } + if (candidatePid < 0) { + break; + } Boolean processStatus = client.getProcessStatus(candidatePid); System.out.printf("Process is currently running? %b\n", processStatus); @@ -273,9 +272,9 @@ public class JobServClient { case "kill": candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } + if (candidatePid < 0) { + break; + } client.killProcess(candidatePid); System.out.println("End process request recieved!"); @@ -283,9 +282,9 @@ public class JobServClient { case "return": candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } + if (candidatePid < 0) { + break; + } int returnCode = client.getProcessReturn(candidatePid); @@ -314,15 +313,15 @@ public class JobServClient { * returns -1 (an invalid PID) if bad index or unparsable int */ private static int getPidArg(String[] args, int index) { - if (args.length < index) { - System.out.println("Improper formatting, try client --help"); - return -1; - } + if (args.length < index) { + System.out.println("Improper formatting, try client --help"); + return -1; + } try { return Integer.parseInt(args[6]); - - } catch (InputMismatchException e) { + + } catch (InputMismatchException e) { System.out.println(args[6] + " is not a valid int, much less a valid pid"); return -1; } diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java new file mode 100644 index 0000000..d1d5383 --- /dev/null +++ b/src/main/java/JobServ/ProcessController.java @@ -0,0 +1,101 @@ +/* + * ProcessController + * + * v1.0 + * + * May 22, 2019 + */ + +package JobServ; + +/* + * ProcessController + * This class wraps a java Process object with metadata + * such as translated PID that exist for this specific API + * as well as general metadata like IO streams. + */ +class ProcessController { + // incremented in constructor + private static int nextPid = 0; + private int pid; + + // TODO: add an api endpoint for streaming client input into + // interactive processes (out of scope for initial API) + private OutputStream output; + private InputStream input; + private Scanner outputScanner; + + private Process process; + + /* + * Constructor + * Takes a command and spawns it in a new process + * Redirects IO streams and assigns a fake PID + */ + public ProcessController(String command) throws IOException { + this.pid = ProcessController.nextPid; + ProcessController.nextPid += 1; + + this.process = Runtime.exec(command); + this.output = this.process.getOutputStream(); + this.input = this.process.getInputStream(); + this.outputScanner = new Scanner(this.input); + this.outputScanner.useDelimieter("\\A"); + } + + /* + * getStatus() + * returns whether or not the process is running + * this isnt a very direct way of getting the information + * The alternative is to use reflection to get into the private UNIXProcess class + * for the PID and to check that against 'ps' or a similar command + * + * TODO: (for future release) return thread state + */ + public Boolean getStatus() { + try { + process.exitValue(); + return true; + } catch (IllegalThreadStateException e) { + return false; + } + } + + /* + * getReturn() + * returns the exit code of the process + * or 256 if process is still running + * (unix/posix defines an exit code as a uint8, so 256 is fair game) + */ + public int getReturn() { + try { + return process.exitValue(); + } catch (IllegalThreadStateException e) { + return 256; + } + } + + /* + * getOutput() + * gets new output from stream + * (TODO: investigate whether this would better be done by ) + */ + public String getOutput() { + String out = ""; + while(scanner.hasNext()) { + out += scanner.next(); + } + + return out; + } + + /* + * kill() + * Cleans up resources and destroys process + */ + public void kill() { + this.input.close(); + this.output.close(); + process.destroy(); + } +} From 0433ead782d0ae8ee7b4e698dc868cb22d18e966 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 22 May 2019 22:25:44 -0700 Subject: [PATCH 35/63] initial sketch for process manager --- src/main/java/JobServ/ProcessController.java | 11 +- src/main/java/JobServ/ProcessManager.java | 278 +++++++++++++++++++ 2 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 src/main/java/JobServ/ProcessManager.java diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index d1d5383..bceb64c 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -43,6 +43,14 @@ class ProcessController { this.outputScanner.useDelimieter("\\A"); } + /* + * getPid() + * returns translated pid of this process + */ + public int getPid() { + return this.pid; + } + /* * getStatus() * returns whether or not the process is running @@ -77,8 +85,7 @@ class ProcessController { /* * getOutput() - * gets new output from stream - * (TODO: investigate whether this would better be done by ) + * gets output from process */ public String getOutput() { String out = ""; diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java new file mode 100644 index 0000000..4acb02e --- /dev/null +++ b/src/main/java/JobServ/ProcessManager.java @@ -0,0 +1,278 @@ +/* + * ProcessManager + * + * v1.0 + * + * May 22, 2019 + */ + +package JobServ; + +import java.util.concurrent.Future; + +/* + * Holds a list of ProcessControllers and controls access to them via mutex + * Additionally, starts and manages a background thread that clears finished processes from the arraylist + */ +class ProcessManager { + // TODO: LOCK_TIMEOUT should be defined in a configuration management system + private final int LOCK_TIMEOUT = 5; // seconds + private ArrayList processQueue; + private Boolean processQueueMutex = false; + private Thread backgroundProcessCleaner; + private ExecutorService threadPool = Executors.newCachedThreadPool(); + + private Callable getLockCallable = new Callable() { + public void Object call() { + while(processQueueMutex){ + continue; // spin! + } + + processQueueMutex = true; + } + } + + /* + * Constructor + * initializes process queue and start the background process checking daemon + */ + public ProcessManager() { + processQueue = new ArrayList(); + // TODO: In a long running server over a large period of time + // It is possible that the streams used to redirect IO in the + // Processes may become a significant use of resources. + // In this case a background thread should be called to periodically + // remove dead ProcessControllers after calling kill() on them. + } + + /* + * newProcess() + * Takes a command and returns the translated pid of a new process + * Returns -1 if getLock fails + */ + public int newProcess(String command) { + /* + * TRADEOFF: Could initialize new ProcessController out here + * Pro: would minimize time spent in critical section + * Con: what if initialization goes through but we dont get the lock + * we would essentially have a dangling untrackable process + * which likely changed system state before it was killed. + */ + + // Enter critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + // (lock was not grabbed) + System.err.println("Timeout starting new job '%s': " + e.getMessage, command); + return -1 + } + + ProcessController newProc = ProcessController(command); + this.processQueue.add(newProc); + + // Exit critical section + this.releaseLock(); + + return newProc.getPid(); + } + + /* + * getProcessStatus() + * returns whether or not a process is running. + * 0: running + * 1: not running + * 2: doesnt exist + * 3: couldnt grab lock + */ + public int getProcessStatus(int pid) { + // Enter critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + // lock could not be grabbed before timeout + System.err.println("Timeout getting process status for %s: " + e.getMessage(), + Integer.toString(pid)); + return 3; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + this.releaseLock(); + // release lock on finding process + return iter.getStatus(); + } + } + + // process must not exist + this.releaseLock(); + return 2; + } + + /* + * getProcessReturn() + * returns a code 0-255, or 256 if process still running + * additionally, returns 257 if lock not grabbable AND + * a 258 if process doesnt exist. + */ + public int getProcessReturn(int pid) { + // Enter Critical section + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout getting process return for %s: " + e.getMessage(), + Integer.toString(pid)); + return 257; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + this.releaseLock(); + return iter.getReturn(); + } + } + + this.releaseLock(); + return 258; + } + + /* + * getProcessOutput() + * returns output of process 'pid' + * or returns description of error + */ + public String getProcessOutput(int pid) { + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout getting process output for %s: " + e.getMessage(), + Integer.toString()); + return "[-] ERROR: Timeout grabbing lock to access process information"; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + output = iter.getOutput(); + this.releaseLock(); + return output; + } + } + + this.releaseLock(); + return "[-] ERROR: Process not found" + } + + /* + * killProcess() + * returns false if couldnt grab lock + * ALSO RETURNS TRUE IF PROCESS DOESNT EXIST + */ + public Boolean killProcess(int pid) { + try { + this.getLock(); + + } catch (TimeoutException e) { + System.err.println("Timeout killing process: " + e.getMessage); + return false; + } + + for (ProcessController iter : this.processQueue) { + if (iter.getPid() == pid) { + iter.kill(); + break; + } + } + + this.releaseLock(); + return true; + } + + /* + * cleanProcessQueue() + * represents a background thread that sits and cleans finished processes + */ + private void cleanProcessQueue() { + while(true){ + try { + this.getLock(); + + } catch (TimeoutException e) { + continue; + } + + for (ProcessController iter : this.processQueue) { + if(!iter.getStatus()) { + iter.kill(); + this.processQueue.remove(iter); + } + } + + this.releaseLock(); + Thread.sleep(5000); + } + } + + /* + * getLock() + * Locks access to this.processQueue + * Waits for a predefined timeout period and then grabs the mutex + * Throws TimeoutException when it fails to get the lock. + */ + private synchronized void getLock() throws TimeoutException { + try { + Future future = executor.submit(task); + void result = future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); + + } catch (InterruptedException e) { + System.err.println("[!] ERROR: " + e.getMessage()); + throw new TimeoutException(); + // rethrowing a timeout exception tells the calling process that they dont have the lock + + } catch (ExecutionException e) { + System.err.println("[!] ERROR: " + e.getMessage()); + throw new TimeoutException(); + + // cancel the attempt to grab the lock + } finally { + future.cancel(true); + } + + /* + * TODO: touch of tech debt here + * There should honestly be an + * operation retry queue for ops + * That dont get the lock in time. + * + * This would require a scheduler + * that manages a queue of callbacks + * This scheduler would also likely + * mediate access to the ProcessManager + * object for fresh calls as well. + */ + } + + /* + * releaseLock() + * releases mutex so other threads can operate on processqueue + */ + private void releaseLock() { + this.processQueueMutex = false; + } + + /* + * shutdown() + * called (eventually) by the grpc shutdown hook + * (AKA when user hits control c in the shell) + * releases resources held in the processController objects + */ + private void shutdown() { + this.getLock(); + for (ProcessController p : this.processQueue) { + p.kill(); // exit threads, release IO streams, etc. + } + } +} From 7d90f1c87f4ff5a24563c32ffdef246e31fbcf99 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 22 May 2019 22:45:56 -0700 Subject: [PATCH 36/63] hashtable is much better for quick access in critical sections than iterating over a list of possibilities --- src/main/java/JobServ/ProcessManager.java | 93 +++++++++-------------- 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 4acb02e..bb8f979 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -17,9 +17,8 @@ import java.util.concurrent.Future; class ProcessManager { // TODO: LOCK_TIMEOUT should be defined in a configuration management system private final int LOCK_TIMEOUT = 5; // seconds - private ArrayList processQueue; - private Boolean processQueueMutex = false; - private Thread backgroundProcessCleaner; + private HashTable processQueue; + private Boolean processQueueMutex = false; private ExecutorService threadPool = Executors.newCachedThreadPool(); private Callable getLockCallable = new Callable() { @@ -37,12 +36,15 @@ class ProcessManager { * initializes process queue and start the background process checking daemon */ public ProcessManager() { - processQueue = new ArrayList(); - // TODO: In a long running server over a large period of time - // It is possible that the streams used to redirect IO in the - // Processes may become a significant use of resources. - // In this case a background thread should be called to periodically - // remove dead ProcessControllers after calling kill() on them. + processQueue = new HashTable(); + /* TODO: In a long running server over a large period of time + * It is possible that the streams used to redirect IO in the + * Processes may become a significant use of resources. + * In this case a background thread should be called to periodically + * remove dead ProcessControllers after calling kill() on them. + * + * (grab lock, iterate over map, remove processes that are done executing, store exit codes, release lock, sleep, repeat) + */ } /* @@ -70,7 +72,7 @@ class ProcessManager { } ProcessController newProc = ProcessController(command); - this.processQueue.add(newProc); + this.processes.map(newProc.getPid(), newProc); // Exit critical section this.releaseLock(); @@ -87,6 +89,8 @@ class ProcessManager { * 3: couldnt grab lock */ public int getProcessStatus(int pid) { + int status; + // Enter critical section try { this.getLock(); @@ -98,12 +102,11 @@ class ProcessManager { return 3; } - for (ProcessController iter : this.processQueue) { - if (iter.getPid() == pid) { - this.releaseLock(); - // release lock on finding process - return iter.getStatus(); - } + ProcessController candidate = this.processMap.get(pid); + if (candidate != null) { + status = candidate.getStatus(); + this.releaseLock(); + return status; } // process must not exist @@ -118,6 +121,8 @@ class ProcessManager { * a 258 if process doesnt exist. */ public int getProcessReturn(int pid) { + int ret; + // Enter Critical section try { this.getLock(); @@ -128,11 +133,11 @@ class ProcessManager { return 257; } - for (ProcessController iter : this.processQueue) { - if (iter.getPid() == pid) { - this.releaseLock(); - return iter.getReturn(); - } + ProcessController candidate = this.processMap.get(pid); + if (candidate != null) { + ret = candidate.getReturn(); + this.releaseLock(); + return ret; } this.releaseLock(); @@ -145,6 +150,8 @@ class ProcessManager { * or returns description of error */ public String getProcessOutput(int pid) { + String output; + try { this.getLock(); @@ -154,12 +161,11 @@ class ProcessManager { return "[-] ERROR: Timeout grabbing lock to access process information"; } - for (ProcessController iter : this.processQueue) { - if (iter.getPid() == pid) { - output = iter.getOutput(); - this.releaseLock(); - return output; - } + ProcessController candidate = this.processMap.get(pid); + if (candidate != null) { + output = iter.getOutput(); + this.releaseLock(); + return output; } this.releaseLock(); @@ -180,42 +186,15 @@ class ProcessManager { return false; } - for (ProcessController iter : this.processQueue) { - if (iter.getPid() == pid) { - iter.kill(); - break; - } + ProcessController candidate = this.processMap.get(pid); + if (candidate != null) { + candidate.kill(); } this.releaseLock(); return true; } - /* - * cleanProcessQueue() - * represents a background thread that sits and cleans finished processes - */ - private void cleanProcessQueue() { - while(true){ - try { - this.getLock(); - - } catch (TimeoutException e) { - continue; - } - - for (ProcessController iter : this.processQueue) { - if(!iter.getStatus()) { - iter.kill(); - this.processQueue.remove(iter); - } - } - - this.releaseLock(); - Thread.sleep(5000); - } - } - /* * getLock() * Locks access to this.processQueue From 9754f23fd8bf0a84cfa47c35f48cebc8f449b378 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Wed, 22 May 2019 23:51:37 -0700 Subject: [PATCH 37/63] fix typoes, uncaught exceptions, started unit tests for ProcessManager --- src/main/java/JobServ/ProcessController.java | 28 +++-- src/main/java/JobServ/ProcessManager.java | 85 ++++++++----- .../JobServ/JobServerAuthenticationTest.java | 114 +++++++++--------- src/test/java/JobServ/ProcessManagerTest.java | 49 ++++++++ 4 files changed, 178 insertions(+), 98 deletions(-) create mode 100644 src/test/java/JobServ/ProcessManagerTest.java diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index bceb64c..2b69c51 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -8,6 +8,11 @@ package JobServ; +import java.util.Scanner; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + /* * ProcessController * This class wraps a java Process object with metadata @@ -36,11 +41,11 @@ class ProcessController { this.pid = ProcessController.nextPid; ProcessController.nextPid += 1; - this.process = Runtime.exec(command); + this.process = Runtime.getRuntime().exec(command); this.output = this.process.getOutputStream(); this.input = this.process.getInputStream(); this.outputScanner = new Scanner(this.input); - this.outputScanner.useDelimieter("\\A"); + this.outputScanner.useDelimiter("\\A"); } /* @@ -60,12 +65,12 @@ class ProcessController { * * TODO: (for future release) return thread state */ - public Boolean getStatus() { + public int getStatus() { try { process.exitValue(); - return true; + return 1; } catch (IllegalThreadStateException e) { - return false; + return 0; } } @@ -89,8 +94,8 @@ class ProcessController { */ public String getOutput() { String out = ""; - while(scanner.hasNext()) { - out += scanner.next(); + while(outputScanner.hasNext()) { + out += outputScanner.next(); } return out; @@ -101,8 +106,13 @@ class ProcessController { * Cleans up resources and destroys process */ public void kill() { - this.input.close(); - this.output.close(); + try { + this.input.close(); + this.output.close(); + } catch (IOException e) { + // streams already closed + } + process.destroy(); } } diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index bb8f979..7e53e58 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -9,41 +9,52 @@ package JobServ; import java.util.concurrent.Future; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.ExecutionException; +import java.util.HashMap; +import java.util.Iterator; +import java.io.IOException; /* + * ProcessManager * Holds a list of ProcessControllers and controls access to them via mutex - * Additionally, starts and manages a background thread that clears finished processes from the arraylist + * Mutex Timeout is declared here as well. */ class ProcessManager { // TODO: LOCK_TIMEOUT should be defined in a configuration management system private final int LOCK_TIMEOUT = 5; // seconds - private HashTable processQueue; + private HashMap processMap; private Boolean processQueueMutex = false; private ExecutorService threadPool = Executors.newCachedThreadPool(); - private Callable getLockCallable = new Callable() { - public void Object call() { + private Callable lockCallable = new Callable() { + public Object call() { while(processQueueMutex){ continue; // spin! } processQueueMutex = true; + return 1; } - } + }; /* * Constructor * initializes process queue and start the background process checking daemon */ public ProcessManager() { - processQueue = new HashTable(); + processMap = new HashMap(); /* TODO: In a long running server over a large period of time * It is possible that the streams used to redirect IO in the * Processes may become a significant use of resources. * In this case a background thread should be called to periodically * remove dead ProcessControllers after calling kill() on them. * - * (grab lock, iterate over map, remove processes that are done executing, store exit codes, release lock, sleep, repeat) + * (grab lock, iterate over map, remove finished processes, store exit codes, release lock, sleep, repeat) */ } @@ -51,6 +62,7 @@ class ProcessManager { * newProcess() * Takes a command and returns the translated pid of a new process * Returns -1 if getLock fails + * Returns -2 if controller throws an IOException */ public int newProcess(String command) { /* @@ -61,23 +73,29 @@ class ProcessManager { * which likely changed system state before it was killed. */ - // Enter critical section try { + // Enter critical section this.getLock(); + ProcessController newProc = new ProcessController(command); + this.processMap.put(newProc.getPid(), newProc); + + // Exit critical section + this.releaseLock(); + + return newProc.getPid(); + } catch (TimeoutException e) { // (lock was not grabbed) - System.err.println("Timeout starting new job '%s': " + e.getMessage, command); - return -1 + System.err.println("Timeout starting new job: " + e.getMessage()); + return -1; + + } catch (IOException e) { + // (lock was grabbed) + this.releaseLock(); + System.err.println("ProcessController couldnt start process: " + e.getMessage()); + return -2; } - - ProcessController newProc = ProcessController(command); - this.processes.map(newProc.getPid(), newProc); - - // Exit critical section - this.releaseLock(); - - return newProc.getPid(); } /* @@ -97,8 +115,7 @@ class ProcessManager { } catch (TimeoutException e) { // lock could not be grabbed before timeout - System.err.println("Timeout getting process status for %s: " + e.getMessage(), - Integer.toString(pid)); + System.err.println("Timeout getting process status: " + e.getMessage()); return 3; } @@ -128,8 +145,7 @@ class ProcessManager { this.getLock(); } catch (TimeoutException e) { - System.err.println("Timeout getting process return for %s: " + e.getMessage(), - Integer.toString(pid)); + System.err.println("Timeout getting process return: " + e.getMessage()); return 257; } @@ -156,20 +172,19 @@ class ProcessManager { this.getLock(); } catch (TimeoutException e) { - System.err.println("Timeout getting process output for %s: " + e.getMessage(), - Integer.toString()); + System.err.println("Timeout getting process output: " + e.getMessage()); return "[-] ERROR: Timeout grabbing lock to access process information"; } ProcessController candidate = this.processMap.get(pid); if (candidate != null) { - output = iter.getOutput(); + output = candidate.getOutput(); this.releaseLock(); return output; } this.releaseLock(); - return "[-] ERROR: Process not found" + return "[-] ERROR: Process not found"; } /* @@ -182,7 +197,7 @@ class ProcessManager { this.getLock(); } catch (TimeoutException e) { - System.err.println("Timeout killing process: " + e.getMessage); + System.err.println("Timeout killing process: " + e.getMessage()); return false; } @@ -202,9 +217,10 @@ class ProcessManager { * Throws TimeoutException when it fails to get the lock. */ private synchronized void getLock() throws TimeoutException { + Future future = this.threadPool.submit(this.lockCallable); + try { - Future future = executor.submit(task); - void result = future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); + future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { System.err.println("[!] ERROR: " + e.getMessage()); @@ -249,9 +265,14 @@ class ProcessManager { * releases resources held in the processController objects */ private void shutdown() { - this.getLock(); - for (ProcessController p : this.processQueue) { - p.kill(); // exit threads, release IO streams, etc. + this.processQueueMutex = true; + + Iterator> iterator = this.processMap.entrySet().iterator(); + while (iterator.hasNext()) { + HashMap.Entry entry = iterator.next(); + + entry.getValue().kill(); + iterator.remove(); } } } diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index 2bec0a9..3a5c099 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -49,7 +49,7 @@ import org.mockito.ArgumentMatchers; public class JobServerAuthenticationTest { private final String projectRoot = ""; - + // Authorized client key/cert/ca private final String clientCa = projectRoot + "resources/client/ca.crt"; private final String clientKey = projectRoot + "resources/client/private.pem"; @@ -73,65 +73,65 @@ public class JobServerAuthenticationTest { // was setUp able to use SSL Certs private Boolean serverSslInitialized = true; private Boolean clientSslInitialized = true; - + /* * test constructor * generates both clients and the server */ public JobServerAuthenticationTest() throws Exception { - - try { - // generate SSL contexts - SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), - new File(serverKey)); - serverContextBuilder.trustManager(new File(clientCa)); - serverContextBuilder.clientAuth(ClientAuth.REQUIRE); - this.server = new JobServServer(GrpcSslContexts.configure(serverContextBuilder).build(), 8448); - this.serverSslInitialized = true; - - } catch (SSLException e) { - this.serverSslInitialized = false; - System.err.println(e.getMessage()); - - } catch (IOException e) { - this.serverSslInitialized = false; - System.err.println(e.getMessage()); - } + try { + // generate SSL contexts + SslContextBuilder serverContextBuilder = SslContextBuilder.forServer(new File(serverCert), + new File(serverKey)); + serverContextBuilder.trustManager(new File(clientCa)); + serverContextBuilder.clientAuth(ClientAuth.REQUIRE); - // generate ssl for clients + this.server = new JobServServer(GrpcSslContexts.configure(serverContextBuilder).build(), 8448); + this.serverSslInitialized = true; + + } catch (SSLException e) { + this.serverSslInitialized = false; + System.err.println(e.getMessage()); + + } catch (IOException e) { + this.serverSslInitialized = false; + System.err.println(e.getMessage()); + } + + // generate ssl for clients if (this.serverSslInitialized) { - try { - SslContextBuilder goodClientBuilder = GrpcSslContexts.forClient(); - goodClientBuilder.trustManager(new File(serverCa)); - goodClientBuilder.keyManager(new File(clientCert), new File(clientKey)); + try { + SslContextBuilder goodClientBuilder = GrpcSslContexts.forClient(); + goodClientBuilder.trustManager(new File(serverCa)); + goodClientBuilder.keyManager(new File(clientCert), new File(clientKey)); + + SslContextBuilder badClientBuilder = GrpcSslContexts.forClient(); + badClientBuilder.trustManager(new File(serverCa)); + badClientBuilder.keyManager(new File(badCert), new File(badKey)); + + ManagedChannel goodChannel = NettyChannelBuilder.forAddress("localhost", 8448) + .sslContext(goodClientBuilder.build()) + .directExecutor() + .build(); - SslContextBuilder badClientBuilder = GrpcSslContexts.forClient(); - badClientBuilder.trustManager(new File(serverCa)); - badClientBuilder.keyManager(new File(badCert), new File(badKey)); - - ManagedChannel goodChannel = NettyChannelBuilder.forAddress("localhost", 8448) - .sslContext(goodClientBuilder.build()) - .directExecutor() - .build(); - - ManagedChannel badChannel = NettyChannelBuilder.forAddress("localhost", 8448) - .sslContext(badClientBuilder.build()) - .directExecutor() - .build(); - - goodClient = new JobServClient(goodChannel); - badClient = new JobServClient(badChannel); - this.clientSslInitialized = true; - - } catch (SSLException e) { - this.clientSslInitialized = false; - System.err.println(e.getMessage()); - } + ManagedChannel badChannel = NettyChannelBuilder.forAddress("localhost", 8448) + .sslContext(badClientBuilder.build()) + .directExecutor() + .build(); + + goodClient = new JobServClient(goodChannel); + badClient = new JobServClient(badChannel); + this.clientSslInitialized = true; + + } catch (SSLException e) { + this.clientSslInitialized = false; + System.err.println(e.getMessage()); + } - } else { - this.clientSslInitialized = false; - } + } else { + this.clientSslInitialized = false; + } } /* @@ -141,14 +141,14 @@ public class JobServerAuthenticationTest { */ @Test public void certificateAuthenticationTest() { - assertEquals(true, serverSslInitialized); - assertEquals(true, clientSslInitialized); + assertEquals(true, serverSslInitialized); + assertEquals(true, clientSslInitialized); - int result = badClient.sendNewJobMessage("test command"); - assertEquals(-2, result); + int result = badClient.sendNewJobMessage("test command"); + assertEquals(-2, result); - result = goodClient.sendNewJobMessage("test command"); - Boolean assertCondition = result == -2; - assertEquals(assertCondition, false); + result = goodClient.sendNewJobMessage("test command"); + Boolean assertCondition = result == -2; + assertEquals(assertCondition, false); } } diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java new file mode 100644 index 0000000..b6d9b45 --- /dev/null +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -0,0 +1,49 @@ +/* + * ProcessManagerTest + * + * v1.0 + * + * May 22, 2019 + */ + +package JobServ; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import static org.junit.Assert.assertEquals; + + +/* + * ProcessManagerTest + * Class that performs positive and negative unit tests + * of every public method in ProcessManager. This not + * only unit tests ProcessManager but also integration + * tests it with ProcessController. + */ +public class ProcessManagerTest { + ProcessManager manager; + + /* + * ProcessManagerTest constructor + * initializes the process manager + */ + public ProcessManagerTest() { + manager = new ProcessManager(); + } + + /* + * addProcessTest() + * positive unit test for newProcess + */ + @Test + public void addProcessesTest() { + int pid1 = manager.newProcess("ping google"); + assertEquals(0, pid1); + + int pid2 = manager.newProcess("ping google"); + assertEquals(1, pid2); + } + +} + From d9beaff898d0d923a97c4f49150a0efead6af4f8 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 01:26:28 -0700 Subject: [PATCH 38/63] added unit tst for getProcessStatus(), fixed scoping issue in processmanager --- src/main/java/JobServ/ProcessManager.java | 2 +- src/test/java/JobServ/ProcessManagerTest.java | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 7e53e58..1679f51 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -264,7 +264,7 @@ class ProcessManager { * (AKA when user hits control c in the shell) * releases resources held in the processController objects */ - private void shutdown() { + public void shutdown() { this.processQueueMutex = true; Iterator> iterator = this.processMap.entrySet().iterator(); diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index b6d9b45..505c03f 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -38,12 +38,27 @@ public class ProcessManagerTest { */ @Test public void addProcessesTest() { - int pid1 = manager.newProcess("ping google"); - assertEquals(0, pid1); + int pid1 = manager.newProcess("ping google.com"); + int pid2 = manager.newProcess("ping google.com"); + assertEquals(2, pid2); + assertEquals(1, pid1); - int pid2 = manager.newProcess("ping google"); - assertEquals(1, pid2); + manager.shutdown(); } + /* + * getStatusTest + * positive unit test for getStatus + */ + @Test + public void getStatusTest() { + int pid1 = manager.newProcess("ping google.com"); + assertEquals(0, pid1); + + int status = manager.getProcessStatus(pid1); + assertEquals(0, status); + + manager.shutdown(); + } } From 7d8f8111c82b99a8dec2c7d7528853a082908daf Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 14:00:15 -0700 Subject: [PATCH 39/63] unit tests for ProcessManager --- src/main/java/JobServ/ProcessManager.java | 10 +- src/test/java/JobServ/ProcessManagerTest.java | 130 ++++++++++++++++-- 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 1679f51..7793486 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -189,16 +189,16 @@ class ProcessManager { /* * killProcess() - * returns false if couldnt grab lock - * ALSO RETURNS TRUE IF PROCESS DOESNT EXIST + * returns 1 if couldnt grab lock + * ALSO RETURNS 0 IF PROCESS DOESNT EXIST */ - public Boolean killProcess(int pid) { + public int killProcess(int pid) { try { this.getLock(); } catch (TimeoutException e) { System.err.println("Timeout killing process: " + e.getMessage()); - return false; + return 1; } ProcessController candidate = this.processMap.get(pid); @@ -207,7 +207,7 @@ class ProcessManager { } this.releaseLock(); - return true; + return 0; } /* diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index 505c03f..fac1e4c 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -11,7 +11,9 @@ package JobServ; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; /* @@ -38,27 +40,137 @@ public class ProcessManagerTest { */ @Test public void addProcessesTest() { - int pid1 = manager.newProcess("ping google.com"); - int pid2 = manager.newProcess("ping google.com"); - assertEquals(2, pid2); - assertEquals(1, pid1); + int pid = manager.newProcess("sleep 1"); + assertNotEquals(-2, pid); manager.shutdown(); } /* * getStatusTest - * positive unit test for getStatus + * unit test for getStatus */ @Test public void getStatusTest() { - int pid1 = manager.newProcess("ping google.com"); - assertEquals(0, pid1); - - int status = manager.getProcessStatus(pid1); + int pid = manager.newProcess("sleep 1"); + int status = manager.getProcessStatus(pid); assertEquals(0, status); manager.shutdown(); } + + /* + * getOldStatusTest + * do finished processes return 1 + */ + @Test + public void getOldStatusTest() { + int pid = manager.newProcess("echo 'test'"); + + try{ + Thread.sleep(200); + } catch (InterruptedException e) { + // + } + + int status = manager.getProcessStatus(pid); + assertEquals(1, status); + + manager.shutdown(); + } + + /* + * getUnknownStatusTest() + * ensures 2 is returned when a status is not known + */ + @Test + public void getUnknownStatusTest() { + int status = manager.getProcessStatus(400); + assertEquals(2, status); + } + + /* + * getReturnTest() + * test of process returns + */ + @Test + public void getReturnTest() { + int pid = manager.newProcess("sleep .5"); + int ret = manager.getProcessReturn(pid); + assertEquals(256, ret); + + try { + Thread.sleep(550); + } catch (InterruptedException e) { + // + } + + ret = manager.getProcessReturn(pid); + assertNotEquals(ret, 256); + assertNotEquals(ret, 257); + assertNotEquals(ret, 258); + + manager.shutdown(); + } + + /* + * getUNknownProcessReturn + * tests process return for unknown processes + */ + @Test + public void getUnknownProcessReturnTest() { + int ret = manager.getProcessReturn(502); + assertEquals(258, ret); + manager.shutdown(); + } + + /* + * getProcessOutputTest() + * verifies output is grabbed correctly from processes + */ + @Test + public void getProcessOutputTest() { + int pid = manager.newProcess("echo test"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // + } + + String out = manager.getProcessOutput(pid); + assertEquals("test\n", out); // calls string.equals() + + manager.shutdown(); + } + + + /* + * getUnknownOutputTest() + * verifies correct information is returned when + * output is requested of an unknown process + */ + @Test + public void getUnknownOutputTest() { + String out = manager.getProcessOutput(532); + assertEquals("[-] ERROR: Process not found", out); + manager.shutdown(); + } + + /* + * killProcessTest() + * ensures killing a process works + */ + @Test + public void killProcessTest() { + int pid = manager.newProcess("sleep 10"); + int ret = manager.killProcess(pid); + + assertEquals(0, ret); + + int status = manager.getProcessStatus(pid); + + assertEquals(1, status); + } } From eb15da6ae228838dc3f8075cd7b83967bcb9d366 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 14:21:40 -0700 Subject: [PATCH 40/63] finished unit tests, better process status reporting --- src/main/java/JobServ/ProcessController.java | 19 +++++++++- src/main/java/JobServ/ProcessManager.java | 36 ++++++++++++------- src/test/java/JobServ/ProcessManagerTest.java | 9 +++-- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index 2b69c51..19aa08b 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -32,6 +32,8 @@ class ProcessController { private Process process; + private Boolean killedManually = false; + /* * Constructor * Takes a command and spawns it in a new process @@ -66,6 +68,10 @@ class ProcessController { * TODO: (for future release) return thread state */ public int getStatus() { + if (this.killedManually) { + return 2; + } + try { process.exitValue(); return 1; @@ -77,10 +83,15 @@ class ProcessController { /* * getReturn() * returns the exit code of the process - * or 256 if process is still running + * 256 if process is still running + * 257 if process was killed manually and no longer exists * (unix/posix defines an exit code as a uint8, so 256 is fair game) */ public int getReturn() { + if (this.killedManually) { + return 257; + } + try { return process.exitValue(); } catch (IllegalThreadStateException e) { @@ -93,6 +104,10 @@ class ProcessController { * gets output from process */ public String getOutput() { + if (this.killedManually) { + return ""; + } + String out = ""; while(outputScanner.hasNext()) { out += outputScanner.next(); @@ -114,5 +129,7 @@ class ProcessController { } process.destroy(); + + this.killedManually = true; } } diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 7793486..3637550 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -103,8 +103,9 @@ class ProcessManager { * returns whether or not a process is running. * 0: running * 1: not running - * 2: doesnt exist - * 3: couldnt grab lock + * 2: killed manually by a client + * 3: doesnt exist + * 4: couldnt grab lock */ public int getProcessStatus(int pid) { int status; @@ -116,7 +117,7 @@ class ProcessManager { } catch (TimeoutException e) { // lock could not be grabbed before timeout System.err.println("Timeout getting process status: " + e.getMessage()); - return 3; + return 4; } ProcessController candidate = this.processMap.get(pid); @@ -128,14 +129,17 @@ class ProcessManager { // process must not exist this.releaseLock(); - return 2; + return 3; } /* * getProcessReturn() - * returns a code 0-255, or 256 if process still running - * additionally, returns 257 if lock not grabbable AND - * a 258 if process doesnt exist. + * returns: + * 0-255: process exit code + * 256: process still running + * 257: process was killed by a client (TODO: list which client connection killed a process) + * 258: process doesnt exist + * 259: couldnt grab lock in time */ public int getProcessReturn(int pid) { int ret; @@ -146,7 +150,7 @@ class ProcessManager { } catch (TimeoutException e) { System.err.println("Timeout getting process return: " + e.getMessage()); - return 257; + return 259; } ProcessController candidate = this.processMap.get(pid); @@ -189,25 +193,33 @@ class ProcessManager { /* * killProcess() - * returns 1 if couldnt grab lock - * ALSO RETURNS 0 IF PROCESS DOESNT EXIST + * returns mirror processStatus + * returns 0 if still running + * returns 1 if process was killed + * returns 2 if process not found + * returns 3 if couldnt grab lock */ public int killProcess(int pid) { + int code; try { this.getLock(); } catch (TimeoutException e) { System.err.println("Timeout killing process: " + e.getMessage()); - return 1; + return 3; } ProcessController candidate = this.processMap.get(pid); if (candidate != null) { candidate.kill(); + code = 1; + + } else { + code = 2; } this.releaseLock(); - return 0; + return code; } /* diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index fac1e4c..4e88ccf 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -86,7 +86,7 @@ public class ProcessManagerTest { @Test public void getUnknownStatusTest() { int status = manager.getProcessStatus(400); - assertEquals(2, status); + assertEquals(3, status); } /* @@ -160,17 +160,20 @@ public class ProcessManagerTest { /* * killProcessTest() * ensures killing a process works + * also tests if getProcessStatus returns 2 */ @Test public void killProcessTest() { int pid = manager.newProcess("sleep 10"); int ret = manager.killProcess(pid); - assertEquals(0, ret); + assertEquals(1, ret); int status = manager.getProcessStatus(pid); - assertEquals(1, status); + assertEquals(2, status); + + manager.shutdown(); } } From d814ff74b018665ee6631be39aa0e662332c2ddd Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 14:30:37 -0700 Subject: [PATCH 41/63] plugged ProcessManager into ShellServerService --- src/main/java/JobServ/ShellServerService.java | 69 +++++++++++++++---- src/main/proto/jobserv.proto | 2 +- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java index f6cb394..7275b12 100644 --- a/src/main/java/JobServ/ShellServerService.java +++ b/src/main/java/JobServ/ShellServerService.java @@ -1,13 +1,13 @@ /* * ShellServerService - * + * * v1.0 - * + * * May 18, 2019 */ package JobServ; -import io.grpc.stub.StreamObserver; +import io.grpc.stub.StreamObserver; /* * The ShellServerService wraps around the protobuf API @@ -15,59 +15,98 @@ import io.grpc.stub.StreamObserver; */ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { + private ProcessManager manager; + + /* + * constructor + * initialized ProcessManager + */ + public ShellServerService() { + manager = new ProcessManager(); + } + + /* + * getStatus + * implements api endpoint as defined in jobserv.proto + */ @Override public void getStatus(PIDMessage request, StreamObserver responder) { - // TODO: Implement job controller, check request type, do the thing + + int status = manager.getProcessStatus(request.getPid()); + StatusMessage reply = StatusMessage.newBuilder() - .setIsRunning(true) + .setProcessStatus(status) .build(); responder.onNext(reply); responder.onCompleted(); } + /* + * getOutput + * implements api endpoint as defined in jobserv.proto + */ @Override public void getOutput(PIDMessage request, StreamObserver responder) { - // TODO: Implement job controller, get output, do the thing + + String output = manager.getProcessOutput(request.getPid()); + OutputMessage reply = OutputMessage.newBuilder() - .setOutput("test data") + .setOutput(output) .build(); responder.onNext(reply); responder.onCompleted(); } + /* + * newJob + * implements api endpoint as defined in jobserv.proto + */ @Override public void newJob(NewJobMessage request, StreamObserver responder) { - // TODO: Implement job controller, Start Job, do the thing + + int newPid = manager.newProcess(request.getCommand()); + PIDMessage reply = PIDMessage.newBuilder() - .setPid(-1) + .setPid(newPid) .build(); responder.onNext(reply); responder.onCompleted(); } + /* + * getReturn + * implements api endpoint as defined in jobserv.proto + */ @Override public void getReturn(PIDMessage request, StreamObserver responder) { - // TODO: Implement job controller, get return code + + int retVal = manager.getProcessReturn(request.getPid()); + ReturnMessage reply = ReturnMessage.newBuilder() - .setProcessReturnCode(277) + .setProcessReturnCode(retVal) .build(); responder.onNext(reply); responder.onCompleted(); } + /* + * killJob + * implements api endpoint as defined in jobserv.proto + */ @Override public void killJob(PIDMessage request, StreamObserver responder) { - // TODO: implement job controller, do the thing - // TODO: kill job here + + int status = manager.getProcessStatus(request.getPid()); + StatusMessage reply = StatusMessage.newBuilder() - .setIsRunning(false) + .setProcessStatus(status) .build(); responder.onNext(reply); responder.onCompleted(); } -} \ No newline at end of file +} diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index e223cd0..351710d 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -16,7 +16,7 @@ service ShellServer { } message StatusMessage { - bool IsRunning = 1; + int32 ProcessStatus = 1; } message ReturnMessage { From 11d85164172340271c35f515a3e5951521f94562 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 16:06:07 -0700 Subject: [PATCH 42/63] Refactored JobServClient into multiple modules --- src/main/java/JobServ/JobServClient.java | 504 ++++++++---------- .../JobServ/JobServClientAPIConnector.java | 203 +++++++ src/main/java/JobServ/ProcessManager.java | 2 +- .../JobServ/JobServerAuthenticationTest.java | 24 +- 4 files changed, 433 insertions(+), 300 deletions(-) create mode 100644 src/main/java/JobServ/JobServClientAPIConnector.java diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index f5e5606..53d3ff8 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -8,324 +8,57 @@ package JobServ; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NettyChannelBuilder; +import io.grpc.ManagedChannel; +import java.util.InputMismatchException; +import io.grpc.ManagedChannelBuilder; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; - import javax.net.ssl.SSLException; -import java.io.File; -import java.util.InputMismatchException; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; +import io.grpc.netty.NettyChannelBuilder; import java.util.Scanner; +import java.io.File; /* * The JobServClient class extends the gRPC stub code * Additionally, it plugs a command line interface into the API code. */ public class JobServClient { + private final String serversideTimeoutErrorMessage = "Timeout locking process control on server\n"+ + "Server could be under heavy load\nConsider trying again."; - /* - * The client should not use the same logging module as the server. - * In a more robust product the server logging module will take advantage of system level - * log aggregators such as journalctl, which the client should not be writing to on the users system - */ - private static final Logger logger = Logger.getLogger(JobServClient.class.getName()); - - private final ManagedChannel channel; - - /* - * blockingStub is used when the client needs to block until the server responds - * the client doesnt nessesarily need to support asynchronously firing off commands - * in this shell-like interface it would be disconcerting to get multiple returns out of order - */ - private final ShellServerGrpc.ShellServerBlockingStub blockingStub; + private JobServClientAPIConnector api; + private String[] programArgs; /* * Constructor - * Spawns a new blockingStub for network operations with the server + * takes program arguments and an api connector object */ - public JobServClient(ManagedChannel channel) { - this.channel = channel; - blockingStub = ShellServerGrpc.newBlockingStub(this.channel); + public JobServClient(String[] args, JobServClientAPIConnector api) { + this.programArgs = args; + this.api = api; } - /* - * shutdown() - * Gets called when you press cntrl+c - * takes at most 5 seconds to close its connection - */ - public void shutdown() throws InterruptedException { - channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); - } - - /* - * getProcessInfo() - * sends the server a request for output from the process identified by 'pid' - * returns process output as string - */ - public String getProcessOutput(int pid) { - logger.info("[+] requesting output"); - - PIDMessage request = PIDMessage.newBuilder() - .setPid(pid) - .build(); - OutputMessage response; - - try { - // blocking network operation - response = blockingStub.getOutput(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for output failed: " + e.getStatus()); - return ""; - } - - return response.getOutput(); - } - - /* - * sendNewJobMessage() - * sends a shell command to the api server - * returns new pid of job - * or -1 if server failed to create job - * or -2 if failed to connect to API - */ - public int sendNewJobMessage(String command) { - // thought of escaping this, but the vulnerability is only client side, from client user input. - logger.info("[+] Sending command to server"); - - NewJobMessage request = NewJobMessage.newBuilder() - .setCommand(command) - .build(); - PIDMessage response; - - try { - // blocking network operation - response = blockingStub.newJob(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for new job failed: " + e.getStatus()); - return -2; - } - - if(response.getPid() == -1) { - logger.log(Level.WARNING, "New job creation failed server side!"); - } - - return response.getPid(); - } - - /* - * getProcessStatus() - * requests running status of process pid - * returns true if process still running else false - */ - public Boolean getProcessStatus(int pid) { - logger.info("[+] Requesting status of a job"); - - PIDMessage request = PIDMessage.newBuilder() - .setPid(pid) - .build(); - StatusMessage response; - - try { - // blocking network operation - response = blockingStub.getStatus(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Request for status failed: " + e.getStatus()); - return false; - } - - return response.getIsRunning(); - } - - /* - * sends PID to server - * returns process exit code - * returns a 0-255 return code or 256 if still running - * or 257 if error in API - */ - public int getProcessReturn(int pid) { - logger.info("[+] Requesting return code of a job"); - - PIDMessage request = PIDMessage.newBuilder() - .setPid(pid) - .build(); - ReturnMessage response; - - try { - // blocking network operation - response = blockingStub.getReturn(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Failed to get return code: " + e.getStatus()); - return 257; - } - - return response.getProcessReturnCode(); - } - - /* - * killProcess() - * send a PID to be killed, function returns nothing - * logs warning if job status comes back still running - */ - public void killProcess(int pid) { - logger.info("[+] Killing a job"); - - PIDMessage request = PIDMessage.newBuilder() - .setPid(pid) - .build(); - StatusMessage response; - - try { - // blocking network operation - response = blockingStub.killJob(request); - } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "(API Failure) Failed to send request: " + e.getStatus()); - return; - } - - if (response.getIsRunning()) { - logger.log(Level.WARNING, "[-] Server failed to kill job!"); - } - } - - /* - * main() - * Client entrypoint - * Parses arguments, initializes client, and calls the correct functions - */ - public static void main(String[] args) throws Exception { - - // check args - if (args.length < 7) { - System.out.println("Usage: $ ./jobserv-client privatekey, cert, truststore, host, port, command, args"); - System.out.println("Or try $ ./jobserv-client help"); - outputHelp(); - return; - } - - JobServClient client; - try { - SslContextBuilder builder = GrpcSslContexts.forClient(); - builder.trustManager(new File(args[2])); - builder.keyManager(new File(args[1]), new File(args[0])); - - ManagedChannel channel = NettyChannelBuilder.forAddress(args[3], Integer.parseInt(args[4])) - .sslContext(builder.build()) - .build(); - client = new JobServClient(channel); - - // Likely bad port - } catch (NumberFormatException e) { - System.out.println("Invalid Port"); - return; - - // bad cert or key format - } catch (SSLException e) { - System.out.println(e.getMessage()); - return; - } - - // declare pid up here so that multiple switch cases can use it - int candidatePid; - // parse remaining args - switch (args[5]) { - case "new": - if (args.length < 6) { - System.out.println("Improper formatting, try client --help"); - break; - } - - String command = ""; - for (int token = 6; token < args.length; token++) { - command += " " + args[token]; - } - - int newProcess = client.sendNewJobMessage(command); - System.out.printf("Process started, assigned pid is %d\n", newProcess); - break; - - case "output": - candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } - - String processOutput = client.getProcessOutput(candidatePid); - System.out.println(processOutput); - break; - - case "status": - candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } - - Boolean processStatus = client.getProcessStatus(candidatePid); - System.out.printf("Process is currently running? %b\n", processStatus); - break; - - case "kill": - candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } - - client.killProcess(candidatePid); - System.out.println("End process request recieved!"); - break; - - case "return": - candidatePid = getPidArg(args, 6); - if (candidatePid < 0) { - break; - } - - int returnCode = client.getProcessReturn(candidatePid); - - if (returnCode == 277) { - System.out.println("Process is still running"); - break; - - } else if (returnCode == 278) { - System.out.println("RPC Call error!"); - break; - - } else { - System.out.printf("Process Exit Code: %d\n", returnCode); - } - - default: - System.out.println("Improper command, try 'help'"); - break; - } - } - /* * getPidArg() * reentrant code was found in all commands except newjob - * this function pulls the pid argument and wraps around the integer case + * this function pulls the pid argument and wraps around the integer cast * returns -1 (an invalid PID) if bad index or unparsable int */ - private static int getPidArg(String[] args, int index) { - if (args.length < index) { + private int getPidArg(int index) { + if (this.programArgs.length < index) { System.out.println("Improper formatting, try client --help"); return -1; } try { - return Integer.parseInt(args[6]); - + return Integer.parseInt(this.programArgs[index]); + } catch (InputMismatchException e) { - System.out.println(args[6] + " is not a valid int, much less a valid pid"); + System.out.println(this.programArgs[index] + " is not a valid int, much less a valid pid"); return -1; } - + } /* @@ -344,4 +77,201 @@ public class JobServClient { System.out.println("... kill (pid)"); System.out.println("Immediately destroys remote process"); } + + /* + * makeNewProcess + * makes a new process + */ + public void makeNewProcess() { + if (this.programArgs.length < 6) { + System.out.println("Improper formatting, try client --help"); + return; + } + + String command = ""; + for (int token = 6; token < this.programArgs.length; token++) { + command += " " + this.programArgs[token]; + } + + int newProcess = this.api.sendNewJobMessage(command); + // TODO: switch here for negative returns + System.out.printf("Process started, assigned pid is %d\n", newProcess); + } + + /* + * getOutput + * gets output from a process + */ + public void getOutput() { + int candidatePid = this.getPidArg(6); + if (candidatePid < 0) { + return; + } + + String processOutput = this.api.getProcessOutput(candidatePid); + System.out.println(processOutput); + } + + /* + * getStatus + * gets the running status of a process + */ + public void getStatus() { + int candidatePid = this.getPidArg(6); + if (candidatePid < 0) { + return; + } + + int processStatus = this.api.getProcessStatus(candidatePid); + switch(processStatus) { + case 0: + System.out.println("Process is running"); + break; + case 1: + System.out.println("Process is not running"); + break; + case 2: + System.out.println("A client killed the process already"); + break; + case 3: + System.out.println("Process does not exist"); + break; + case 4: + System.out.println(this.serversideTimeoutErrorMessage); + break; + } + } + + /* + * killProcess + * kills a process + */ + public void killProcess() { + int candidatePid = this.getPidArg(6); + if (candidatePid < 0) { + return; + } + + int finalStatus = this.api.killProcess(candidatePid); + switch(finalStatus) { + case 0: + System.out.println("Process is still running"); + break; + + case 1: + System.out.println("Process was killed"); + break; + + case 2: + System.out.println("Process does not exist"); + break; + + case 3: + System.out.println(this.serversideTimeoutErrorMessage); + break; + + case 4: + // error logged in API Connector + break; + } + } + + /* + * getReturn + * gets return code from a process + */ + public void getReturn() { + int candidatePid = this.getPidArg(6); + if (candidatePid < 0) { + return; + } + + int returnCode = this.api.getProcessReturn(candidatePid); + + switch(returnCode){ + case 256: + System.out.println("Process is still running"); + break; + case 257: + System.out.println("Process was killed manually by a client"); + break; + case 258: + System.out.println("Process does not exist"); + break; + case 259: + System.out.println(this.serversideTimeoutErrorMessage); + break; + case 260: + // error logged in getProcesReturn + break; + default: + System.out.println("Process Exit Code: " + Integer.toString(returnCode)); + } + } + + /* + * main() + * Client entrypoint + * Parses arguments, initializes client, and calls the correct functions + */ + public static void main(String[] args) throws Exception { + // check args + if (args.length < 7) { + System.out.println("Usage: $ ./jobserv-client privatekey, cert, truststore, host, port, command, args"); + System.out.println("Or try $ ./jobserv-client help"); + outputHelp(); + return; + } + + JobServClientAPIConnector api; + try { + SslContextBuilder builder = GrpcSslContexts.forClient(); + builder.trustManager(new File(args[2])); + builder.keyManager(new File(args[1]), new File(args[0])); + + ManagedChannel channel = NettyChannelBuilder.forAddress(args[3], Integer.parseInt(args[4])) + .sslContext(builder.build()) + .build(); + api = new JobServClientAPIConnector(channel); + + // Likely bad port + } catch (NumberFormatException e) { + System.out.println("Invalid Port"); + return; + + // bad cert or key format + } catch (SSLException e) { + System.out.println(e.getMessage()); + return; + } + + JobServClient client = new JobServClient(args, api); + + // parse remaining args + switch (args[5]) { + case "new": + client.makeNewProcess(); + break; + + case "output": + client.getOutput(); + break; + + case "status": + client.getStatus(); + break; + + case "kill": + client.killProcess(); + break; + + case "return": + client.getReturn(); + break; + + default: + System.out.println("Improper command, try 'help'"); + break; + } + } } diff --git a/src/main/java/JobServ/JobServClientAPIConnector.java b/src/main/java/JobServ/JobServClientAPIConnector.java new file mode 100644 index 0000000..834f98d --- /dev/null +++ b/src/main/java/JobServ/JobServClientAPIConnector.java @@ -0,0 +1,203 @@ +/* + * JobServClientAPIConnector + * + * v1.0 + * + * May 23, 2019 + */ + +package JobServ; + +import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import java.io.File; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/* + * JobServClientAPIConnector + * Starts a connection to the API Connector + * implements functions that send and recieve frm the API + * Refactored into its own module to make the Client interface nicer + * and to allow for a veriety of interfaces to be created + */ +class JobServClientAPIConnector { + private final String serversideTimeoutErrorMessage = "Timeout locking process control on server\n"+ + "Server could be under heavy load\nConsider trying again."; + private final String apiFailureMessage = "Failed while trying to connect to server."; + + /* + * The client should not use the same logging module as the server. + * In a more robust product the server logging module will take advantage of system level + * log aggregators such as journalctl, which the client should not be writing to on the users system + */ + private static final Logger logger = Logger.getLogger(JobServClient.class.getName()); + + private final ManagedChannel channel; + + /* + * blockingStub is used when the client needs to block until the server responds + * the client doesnt nessesarily need to support asynchronously firing off commands + * in this shell-like interface it would be disconcerting to get multiple returns out of order + */ + private final ShellServerGrpc.ShellServerBlockingStub blockingStub; + + /* + * Constructor + * Spawns a new blockingStub for network operations with the server + */ + public JobServClientAPIConnector(ManagedChannel channel) { + this.channel = channel; + blockingStub = ShellServerGrpc.newBlockingStub(this.channel); + } + + /* + * shutdown() + * Gets called when you press cntrl+c + * takes at most 5 seconds to close its connection + */ + public void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + + /* + * getProcessOutput() + * sends the server a request for output from the process identified by 'pid' + * returns process output as string + */ + public String getProcessOutput(int pid) { + logger.info("[+] requesting output"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + OutputMessage response; + + try { + // blocking network operation + response = blockingStub.getOutput(request); + + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus()); + return ""; + } + + return response.getOutput(); + } + + /* + * sendNewJobMessage() + * sends a shell command to the api server + * returns new pid of job + * or -1 if server thread couldnt synchronize before timeout + * or -2 if server failed to create job + * or -3 if client fails to connect + */ + public int sendNewJobMessage(String command) { + // thought of escaping this, but the vulnerability is only client side, from client user input. + logger.info("[+] Sending command to server"); + + NewJobMessage request = NewJobMessage.newBuilder() + .setCommand(command) + .build(); + PIDMessage response; + + try { + // blocking network operation + response = blockingStub.newJob(request); + + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus()); + return -3; + } + + return response.getPid(); + } + + /* + * getProcessStatus() + * requests running status of process pid + * 0: running + * 1: not running + * 2: killed manually by a client + * 3: doesnt exist + * 4: couldnt grab lock + */ + public int getProcessStatus(int pid) { + logger.info("[+] Requesting status of a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + StatusMessage response; + + try { + // blocking network operation + response = blockingStub.getStatus(request); + + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus()); + return -1; + } + + return response.getProcessStatus(); + } + + /* + * sends PID to server + * returns process exit code + * 0-255: process exit code + * 256: process still running + * 257: process was killed by a client + * 258: process doesnt exist + * 259: couldnt grab lock in time + * 260: couldnt connect to API + */ + public int getProcessReturn(int pid) { + logger.info("[+] Requesting return code of a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + ReturnMessage response; + + try { + // blocking network operation + response = blockingStub.getReturn(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus()); + return 260; + } + + return response.getProcessReturnCode(); + } + + /* + * killProcess() + * send a PID to be killed, function returns process status after kill operation + * returns 0 if still running + * returns 1 if process was killed + * returns 2 if process not found + * returns 3 if couldnt grab lock + * returns 4 on API failure + */ + public int killProcess(int pid) { + logger.info("[+] Killing a job"); + + PIDMessage request = PIDMessage.newBuilder() + .setPid(pid) + .build(); + StatusMessage response; + + try { + // blocking network operation + response = blockingStub.killJob(request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus()); + return 4; + } + + return response.getProcessStatus(); + } +} diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 3637550..7cf6c73 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -177,7 +177,7 @@ class ProcessManager { } catch (TimeoutException e) { System.err.println("Timeout getting process output: " + e.getMessage()); - return "[-] ERROR: Timeout grabbing lock to access process information"; + return "[-] SERVER: Timeout grabbing lock to access process information"; } ProcessController candidate = this.processMap.get(pid); diff --git a/src/test/java/JobServ/JobServerAuthenticationTest.java b/src/test/java/JobServ/JobServerAuthenticationTest.java index 3a5c099..781e07b 100644 --- a/src/test/java/JobServ/JobServerAuthenticationTest.java +++ b/src/test/java/JobServ/JobServerAuthenticationTest.java @@ -66,8 +66,8 @@ public class JobServerAuthenticationTest { private final String badCert = projectRoot + "resources/test/test.crt"; // badClient uses unauthorized certs - private JobServClient goodClient; - private JobServClient badClient; + private JobServClientAPIConnector goodClient; + private JobServClientAPIConnector badClient; private JobServServer server; // was setUp able to use SSL Certs @@ -93,7 +93,7 @@ public class JobServerAuthenticationTest { } catch (SSLException e) { this.serverSslInitialized = false; System.err.println(e.getMessage()); - + } catch (IOException e) { this.serverSslInitialized = false; System.err.println(e.getMessage()); @@ -105,25 +105,25 @@ public class JobServerAuthenticationTest { SslContextBuilder goodClientBuilder = GrpcSslContexts.forClient(); goodClientBuilder.trustManager(new File(serverCa)); goodClientBuilder.keyManager(new File(clientCert), new File(clientKey)); - + SslContextBuilder badClientBuilder = GrpcSslContexts.forClient(); badClientBuilder.trustManager(new File(serverCa)); badClientBuilder.keyManager(new File(badCert), new File(badKey)); - + ManagedChannel goodChannel = NettyChannelBuilder.forAddress("localhost", 8448) .sslContext(goodClientBuilder.build()) .directExecutor() - .build(); + .build(); ManagedChannel badChannel = NettyChannelBuilder.forAddress("localhost", 8448) .sslContext(badClientBuilder.build()) .directExecutor() .build(); - - goodClient = new JobServClient(goodChannel); - badClient = new JobServClient(badChannel); + + goodClient = new JobServClientAPIConnector(goodChannel); + badClient = new JobServClientAPIConnector(badChannel); this.clientSslInitialized = true; - + } catch (SSLException e) { this.clientSslInitialized = false; System.err.println(e.getMessage()); @@ -145,10 +145,10 @@ public class JobServerAuthenticationTest { assertEquals(true, clientSslInitialized); int result = badClient.sendNewJobMessage("test command"); - assertEquals(-2, result); + assertEquals(-3, result); result = goodClient.sendNewJobMessage("test command"); - Boolean assertCondition = result == -2; + Boolean assertCondition = result == -3; assertEquals(assertCondition, false); } } From 9eb18219cb63b951b35e523149fde9ca875ed574 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 16:10:43 -0700 Subject: [PATCH 43/63] finished refactoring client for additions to protocol --- src/main/java/JobServ/JobServClient.java | 21 +++++++++++++++++-- .../JobServ/JobServClientAPIConnector.java | 3 --- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 53d3ff8..82cc422 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -94,8 +94,25 @@ public class JobServClient { } int newProcess = this.api.sendNewJobMessage(command); - // TODO: switch here for negative returns - System.out.printf("Process started, assigned pid is %d\n", newProcess); + switch(newProcess) { + case -1: + System.out.println(this.serversideTimeoutErrorMessage); + break; + + case -2: + System.out.println("Server failed to create job, check server logs."); + break; + + case -3: + // error logged by API Connector + break; + + default: + System.out.printf("Process started, assigned pid is %d\n", newProcess); + break; + } + + return; } /* diff --git a/src/main/java/JobServ/JobServClientAPIConnector.java b/src/main/java/JobServ/JobServClientAPIConnector.java index 834f98d..b8f39c3 100644 --- a/src/main/java/JobServ/JobServClientAPIConnector.java +++ b/src/main/java/JobServ/JobServClientAPIConnector.java @@ -10,7 +10,6 @@ package JobServ; import io.grpc.ManagedChannel; import io.grpc.StatusRuntimeException; -import java.io.File; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -23,8 +22,6 @@ import java.util.logging.Logger; * and to allow for a veriety of interfaces to be created */ class JobServClientAPIConnector { - private final String serversideTimeoutErrorMessage = "Timeout locking process control on server\n"+ - "Server could be under heavy load\nConsider trying again."; private final String apiFailureMessage = "Failed while trying to connect to server."; /* From f13e16ebd57a46df6722bdfc967f0d99bd256bdc Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 17:03:40 -0700 Subject: [PATCH 44/63] much friendlier packaging --- README.md | 7 +++---- buildwrapper.sh | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 680a1e8..8b909f2 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ After build, the programs can be found in the staging folder. After changing directory to the 'staging/client' folder or the 'staging/server' folder, either program can be run as follows: ``` -$ ./JobServ/bin/jobserv-server (port) server.crt private.pem ca.crt -$ ./JobServ/bin/jobserv-client private.pem client.crt ca.crt (hostname) (port) (command) (arguments) +$ ./server.sh (port) +$ ./client.sh (hostname) (port) (command) (arguments) ``` alternatively, for guidance: @@ -33,5 +33,4 @@ $ ./JobServ/bin/jobserv-client help # Testing Running the gradle test task, or the buildwrapper will run all junit tests. -Currently that includes a test of certificate based authentication (Mutual TLS) -(TODO: job control module tests) +Currently that includes a test of certificate based authentication (Mutual TLS) and unit tests for the thread safe process control module diff --git a/buildwrapper.sh b/buildwrapper.sh index 9d6a19f..d59dd6a 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -106,3 +106,17 @@ cp resources/client/client.crt staging/client/ cp resources/client/private.pem staging/client/ cp resources/server/ca.crt staging/client/ cp -r resources/* staging/test/ + +echo "[+] Adding wrapper script for client" +# This could also be a .desktop file without much more work. +cat << EOF > staging/client/client.sh + ./JobServ/bin/jobserv-client "${@:4}" +EOF +chown +x staging/client/client.sh + +echo "[+] Adding wrapper script for server" +# This could also be a .desktop file without much more work. +cat << EOF > staging/server/server.sh + ./JobServ/bin/jobserv-server "$1" +EOF +chown +x staging/server/server.sh From 3538cac0d8e2c9ebed543ef700f015d0b87e2ea1 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 17:07:43 -0700 Subject: [PATCH 45/63] distribution instructions in readme, easy run scripts in staging folders --- README.md | 11 +++++++---- buildwrapper.sh | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8b909f2..355f4b3 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,19 @@ After build, the programs can be found in the staging folder. After changing directory to the 'staging/client' folder or the 'staging/server' folder, either program can be run as follows: ``` -$ ./server.sh (port) -$ ./client.sh (hostname) (port) (command) (arguments) +$ ./server (port) +$ ./client (hostname) (port) (command) (arguments) ``` alternatively, for guidance: ``` -$ ./JobServ/bin/jobserv-server -$ ./JobServ/bin/jobserv-client help +$ ./server +$ ./client help ``` +# Distribution +At this point you can copy the staging/client or staging/server folders to any environment in which their Certificate CN's are valid. + # Testing Running the gradle test task, or the buildwrapper will run all junit tests. Currently that includes a test of certificate based authentication (Mutual TLS) and unit tests for the thread safe process control module diff --git a/buildwrapper.sh b/buildwrapper.sh index d59dd6a..2e9712c 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -109,14 +109,14 @@ cp -r resources/* staging/test/ echo "[+] Adding wrapper script for client" # This could also be a .desktop file without much more work. -cat << EOF > staging/client/client.sh - ./JobServ/bin/jobserv-client "${@:4}" +cat << EOF > staging/client/client + ./JobServ/bin/jobserv-client private.pem client.crt ca.crt \$@ EOF -chown +x staging/client/client.sh +chmod +x staging/client/client.sh echo "[+] Adding wrapper script for server" # This could also be a .desktop file without much more work. -cat << EOF > staging/server/server.sh - ./JobServ/bin/jobserv-server "$1" +cat << EOF > staging/server/server + ./JobServ/bin/jobserv-server \$1 server.crt private.pem ca.crt EOF -chown +x staging/server/server.sh +chmod +x staging/server/server.sh From 7b0184cbd34c27a07432524aeb47fb2e61815beb Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 17:24:48 -0700 Subject: [PATCH 46/63] all initialized service objects use the same processmanager now, added shutdown hook to deinitialize processmanager and processcontroller objects --- src/main/java/JobServ/JobServClient.java | 25 +++++++++------ src/main/java/JobServ/JobServServer.java | 31 ++++++++++--------- src/main/java/JobServ/ShellServerService.java | 4 +-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 82cc422..1698ac9 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -66,16 +66,21 @@ public class JobServClient { * writes help information about all commands in the shell to screen */ public static void outputHelp() { - System.out.println("... new (command)"); - System.out.println("Starts a new process on the server"); - System.out.println("... output (pid)"); - System.out.println("Garners output from process on server"); - System.out.println("... status (pid)"); - System.out.println("Returns whether process on server is running"); - System.out.println("... return (pid)"); - System.out.println("Collects return code from remote process"); - System.out.println("... kill (pid)"); - System.out.println("Immediately destroys remote process"); + System.out.println("... new (command)\n"+ + "Starts a new process on the server\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 new echo hello world!\n\n"+ + "... output (pid)\n"+ + "Garners output from process on server\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 output 0\n\n"+ + "... status (pid)\n"+ + "Returns whether process on server is running"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 status 0\n\n"+ + "... return (pid)\n"+ + "Collects return code from remote process\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 return 0\n\n"+ + "... kill (pid)"+ + "Immediately destroys remote process"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0\n\n"+ } /* diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 07e41fa..5e35549 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -32,17 +32,18 @@ public class JobServServer { private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); private Server server; + private ProcessManager manager; /* * Constructor * builds server object */ public JobServServer(SslContext ssl, int port) throws IOException { - this.server = NettyServerBuilder.forPort(port) - .addService(new ShellServerService()) - .sslContext(ssl) - .build() - .start(); + this.server = NettyServerBuilder.forPort(port) + .addService(new ShellServerService(manager)) + .sslContext(ssl) + .build() + .start(); } /* @@ -58,6 +59,7 @@ public class JobServServer { @Override public void run() { logger.info("Shutting down server"); + manager.shutdown(); JobServServer.this.stop(); } }); @@ -100,14 +102,14 @@ public class JobServServer { JobServServer server; try { - SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(args[1]), new File(args[2])); + SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(new File(args[1]), new File(args[2])); - // Mutual TLS trust store and require client auth - sslContextBuilder.trustManager(new File(args[3])); - sslContextBuilder.clientAuth(ClientAuth.REQUIRE); + // Mutual TLS trust store and require client auth + sslContextBuilder.trustManager(new File(args[3])); + sslContextBuilder.clientAuth(ClientAuth.REQUIRE); - server = new JobServServer(GrpcSslContexts.configure(sslContextBuilder).build(), - Integer.parseInt(args[0])); + server = new JobServServer(GrpcSslContexts.configure(sslContextBuilder).build(), + Integer.parseInt(args[0])); } catch (InputMismatchException e) { System.out.println("Invalid port!"); @@ -118,10 +120,11 @@ public class JobServServer { return; } catch (IOException e) { - System.out.println(e.getMessage()); - return; - } + System.out.println(e.getMessage()); + return; + } + System.out.println("JobServ Server Initialized! Connect anytime..."); server.blockUntilShutdown(); } } diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java index 7275b12..4b61fb8 100644 --- a/src/main/java/JobServ/ShellServerService.java +++ b/src/main/java/JobServ/ShellServerService.java @@ -21,8 +21,8 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { * constructor * initialized ProcessManager */ - public ShellServerService() { - manager = new ProcessManager(); + public ShellServerService(ProcessManager manager) { + this.manager = manager } /* From c40f8f0a62e498d42b14a495cbe95db2ddde1cd7 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 17:37:45 -0700 Subject: [PATCH 47/63] couple of typoes --- buildwrapper.sh | 4 ++-- src/main/java/JobServ/JobServClient.java | 2 +- src/main/java/JobServ/JobServServer.java | 1 + src/main/java/JobServ/ShellServerService.java | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/buildwrapper.sh b/buildwrapper.sh index 2e9712c..c391e4c 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -112,11 +112,11 @@ echo "[+] Adding wrapper script for client" cat << EOF > staging/client/client ./JobServ/bin/jobserv-client private.pem client.crt ca.crt \$@ EOF -chmod +x staging/client/client.sh +chmod +x staging/client/client echo "[+] Adding wrapper script for server" # This could also be a .desktop file without much more work. cat << EOF > staging/server/server ./JobServ/bin/jobserv-server \$1 server.crt private.pem ca.crt EOF -chmod +x staging/server/server.sh +chmod +x staging/server/server diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 1698ac9..f348e82 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -80,7 +80,7 @@ public class JobServClient { "example: ./client key.pem cert.crt ca.crt localhost 8448 return 0\n\n"+ "... kill (pid)"+ "Immediately destroys remote process"+ - "example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0\n\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0\n\n"); } /* diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 5e35549..51c226a 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -39,6 +39,7 @@ public class JobServServer { * builds server object */ public JobServServer(SslContext ssl, int port) throws IOException { + this.manager = new ProcessManager(); this.server = NettyServerBuilder.forPort(port) .addService(new ShellServerService(manager)) .sslContext(ssl) diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java index 4b61fb8..953b0e1 100644 --- a/src/main/java/JobServ/ShellServerService.java +++ b/src/main/java/JobServ/ShellServerService.java @@ -22,7 +22,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { * initialized ProcessManager */ public ShellServerService(ProcessManager manager) { - this.manager = manager + this.manager = manager; } /* From f14265fe5bc9c2eebcbceee0396bb3cfccd471f8 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 18:18:13 -0700 Subject: [PATCH 48/63] dont synchronize access to the function that is supposed to synchronize by hand --- src/main/java/JobServ/ProcessManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 7cf6c73..cb3645d 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -228,7 +228,7 @@ class ProcessManager { * Waits for a predefined timeout period and then grabs the mutex * Throws TimeoutException when it fails to get the lock. */ - private synchronized void getLock() throws TimeoutException { + private void getLock() throws TimeoutException { Future future = this.threadPool.submit(this.lockCallable); try { From 8a513164ab75e0f3eeaa2829650de539d28ffe41 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 19:31:03 -0700 Subject: [PATCH 49/63] added a test for Mutex properties of the locking system --- src/main/java/JobServ/ProcessController.java | 4 +- src/main/java/JobServ/ProcessManager.java | 7 +-- src/test/java/JobServ/ProcessManagerTest.java | 47 +++++++++++++++---- .../ProcessManagerTestImplementation.java | 41 ++++++++++++++++ 4 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 src/test/java/JobServ/ProcessManagerTestImplementation.java diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index 19aa08b..69c3bbf 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -124,12 +124,12 @@ class ProcessController { try { this.input.close(); this.output.close(); + this.outputScanner.close(); + this.process.destroy(); } catch (IOException e) { // streams already closed } - process.destroy(); - this.killedManually = true; } } diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index cb3645d..5bae6f0 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -225,10 +225,11 @@ class ProcessManager { /* * getLock() * Locks access to this.processQueue - * Waits for a predefined timeout period and then grabs the mutex + * Waits for a predefined timeout period for mutex to be avail. + * Synchronized so two things cannot grab lock at once. * Throws TimeoutException when it fails to get the lock. */ - private void getLock() throws TimeoutException { + protected synchronized void getLock() throws TimeoutException { Future future = this.threadPool.submit(this.lockCallable); try { @@ -266,7 +267,7 @@ class ProcessManager { * releaseLock() * releases mutex so other threads can operate on processqueue */ - private void releaseLock() { + protected void releaseLock() { this.processQueueMutex = false; } diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index 4e88ccf..89b835a 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -15,6 +15,10 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import java.util.concurrent.Future; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; /* * ProcessManagerTest @@ -24,15 +28,16 @@ import static org.junit.Assert.assertNotEquals; * tests it with ProcessController. */ public class ProcessManagerTest { - ProcessManager manager; + private ProcessManagerTestImplementation manager = new ProcessManagerTestImplementation(); + private ExecutorService threadPool = Executors.newCachedThreadPool(); - /* - * ProcessManagerTest constructor - * initializes the process manager - */ - public ProcessManagerTest() { - manager = new ProcessManager(); - } + // calls a test function that simulates load by holding the lock for a long time + private Callable holdLockSevenSeconds = new Callable() { + public Object call() { + manager.longCallHoldsLock(); + return true; + } + }; /* * addProcessTest() @@ -175,5 +180,31 @@ public class ProcessManagerTest { manager.shutdown(); } + + /* + * asyncLockTimeoutTest + * ensures that two things cannot grab the lock at the same time + */ + @Test + public void asyncLockTimeoutTest() { + Future future = this.threadPool.submit(this.holdLockSevenSeconds); + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + System.err.println("[!!] Thread for async test interrupted!"); + } + + System.err.println("[2] attempting to grab (held) lock"); + int pid = this.manager.newProcess("Test Process"); + assertEquals(-1, pid); + + future.cancel(true); + + int pid2 = this.manager.newProcess("echo test"); + assertNotEquals(-1, pid2); + + manager.shutdown(); + } } diff --git a/src/test/java/JobServ/ProcessManagerTestImplementation.java b/src/test/java/JobServ/ProcessManagerTestImplementation.java new file mode 100644 index 0000000..e9b44a7 --- /dev/null +++ b/src/test/java/JobServ/ProcessManagerTestImplementation.java @@ -0,0 +1,41 @@ +/* + * ProcessManagerTestImplementation + * + * v1.0 + * + * May 23, 2019 + */ + + +package JobServ; + +import java.util.concurrent.TimeoutException; + +/* + * ProcessManagerTestImplementation + * inherits ProcessManager and adds useful functions for testing + */ +class ProcessManagerTestImplementation extends ProcessManager { + + public void longCallHoldsLock() { + try { + super.getLock(); + System.err.println("[1] Long Call Has Lock"); + + // hold lock for 7 seconds, more than double normal timeout. + Thread.sleep(4000); + + super.releaseLock(); + + } catch (TimeoutException e) { + System.err.println("[!!] Long Call wasnt able to grab lock!"); + return; + + } catch (InterruptedException e) { + super.releaseLock(); + System.err.println("[3] Released lock: interrupted"); + return; + } + } + +} From 18f5ffd2890382a22d27ed72621256ca91917dfe Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 19:37:51 -0700 Subject: [PATCH 50/63] elaborate on new tests in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 355f4b3..1ea23f0 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,4 @@ At this point you can copy the staging/client or staging/server folders to any e # Testing Running the gradle test task, or the buildwrapper will run all junit tests. -Currently that includes a test of certificate based authentication (Mutual TLS) and unit tests for the thread safe process control module +Currently that includes a test of certificate based authentication (Mutual TLS), tests for the thread safe process control module, and tests ensureing that only one thread can edit process information at a time. From 4cb9d3a5e1331cfe9af13e0ce1d7499a1d42379a Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 19:40:46 -0700 Subject: [PATCH 51/63] more eloquent readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ea23f0..1797e69 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,12 @@ Simply run the folllowing command: ```shell $ ./buildwrapper.sh ``` -buildwrapper will ask you for details about the client and server, if you are testing this software both CNs can be set to localhost. -buildwrapper will then generate CAs for the Client and Server, and signed certs for the Client and Server. IN addition a seperate, third CA and cert will be generated for testing purposes. -Gradle will then generate protobuf source and compile it with the source for the client and server. -After gradle is completed buildwrapper will organize the sources with their respective certs in the staging folder. In addition to a server folder and a client folder, there will be a test folder which has a copy of all certs and both server and client functionality. The test CA is not trusted by the server or the client by default. As such, the test cert can be used to induce a mutual tls authentication failure. +Buildwrapper will ask you for details about the client and server. If you are testing this software both CNs can be set to localhost. +Buildwrapper will then generate CAs for and signed certs for the Client and Server. In addition a seperate, third CA and cert will be generated for testing purposes. +Gradle will then generate protobuf source and compile it with the java source for the client and server. +After gradle is finished compiling and running the junit tests, buildwrapper will organize the sources with their respective certs in the staging folder. +In addition to a server folder and a client folder, there will be a test folder which has a copy of all certs and both server and client functionality. +The test CA is not trusted by the server or the client by default. As such, the test cert can be used to induce a mutual tls authentication failure. # Running After build, the programs can be found in the staging folder. From 213d48c0873c38afbb63abdd52236405577d8fe0 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 22:13:57 -0700 Subject: [PATCH 52/63] lock individual processes, not the whole queue --- src/main/java/JobServ/ProcessManager.java | 168 ++++++++---------- src/test/java/JobServ/ProcessManagerTest.java | 41 +++-- .../ProcessManagerTestImplementation.java | 11 +- 3 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index 5bae6f0..dd955a1 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.HashMap; import java.util.Iterator; @@ -26,28 +27,29 @@ import java.io.IOException; */ class ProcessManager { // TODO: LOCK_TIMEOUT should be defined in a configuration management system - private final int LOCK_TIMEOUT = 5; // seconds - private HashMap processMap; - private Boolean processQueueMutex = false; + private final int LOCK_TIMEOUT = 2; // seconds + + /* + * The significance of the concurrent hash map is that an in process + * update will not leave it in an unusable state like it will a normal + * HashMap. It is still up to the programmer in this instance to make + * sure that there are no concurrent operations done to the ProcessControllers + * Themselves. The last thing we want is to throw NPEs or whatnot when + * accessing a process destroyed mid read by another thread. + * Hence getLock(...) and lockMap controlling access to individual entries in + * processMap + */ + protected ConcurrentHashMap processMap; + protected ConcurrentHashMap lockMap; private ExecutorService threadPool = Executors.newCachedThreadPool(); - private Callable lockCallable = new Callable() { - public Object call() { - while(processQueueMutex){ - continue; // spin! - } - - processQueueMutex = true; - return 1; - } - }; - /* * Constructor * initializes process queue and start the background process checking daemon */ public ProcessManager() { - processMap = new HashMap(); + processMap = new ConcurrentHashMap(); + lockMap = new ConcurrentHashMap(); /* TODO: In a long running server over a large period of time * It is possible that the streams used to redirect IO in the * Processes may become a significant use of resources. @@ -61,39 +63,21 @@ class ProcessManager { /* * newProcess() * Takes a command and returns the translated pid of a new process - * Returns -1 if getLock fails + * Returns -1 if getLock fails TODO: REMOVE * Returns -2 if controller throws an IOException */ public int newProcess(String command) { - /* - * TRADEOFF: Could initialize new ProcessController out here - * Pro: would minimize time spent in critical section - * Con: what if initialization goes through but we dont get the lock - * we would essentially have a dangling untrackable process - * which likely changed system state before it was killed. - */ try { - // Enter critical section - this.getLock(); - ProcessController newProc = new ProcessController(command); + this.lockMap.put(newProc.getPid(), true); this.processMap.put(newProc.getPid(), newProc); - // Exit critical section - this.releaseLock(); - + this.releaseLock(newProc.getPid()); return newProc.getPid(); - } catch (TimeoutException e) { - // (lock was not grabbed) - System.err.println("Timeout starting new job: " + e.getMessage()); - return -1; - } catch (IOException e) { - // (lock was grabbed) - this.releaseLock(); - System.err.println("ProcessController couldnt start process: " + e.getMessage()); + System.err.println("Couldnt Spawn New Command: " + e.getMessage()); return -2; } } @@ -108,11 +92,10 @@ class ProcessManager { * 4: couldnt grab lock */ public int getProcessStatus(int pid) { - int status; - - // Enter critical section try { - this.getLock(); + if(!this.getLock(pid)) { + return 3; + } } catch (TimeoutException e) { // lock could not be grabbed before timeout @@ -121,15 +104,9 @@ class ProcessManager { } ProcessController candidate = this.processMap.get(pid); - if (candidate != null) { - status = candidate.getStatus(); - this.releaseLock(); - return status; - } - - // process must not exist - this.releaseLock(); - return 3; + int status = candidate.getStatus(); + this.releaseLock(pid); + return status; } /* @@ -142,11 +119,10 @@ class ProcessManager { * 259: couldnt grab lock in time */ public int getProcessReturn(int pid) { - int ret; - - // Enter Critical section try { - this.getLock(); + if(!this.getLock(pid)) { + return 258; + } } catch (TimeoutException e) { System.err.println("Timeout getting process return: " + e.getMessage()); @@ -154,14 +130,9 @@ class ProcessManager { } ProcessController candidate = this.processMap.get(pid); - if (candidate != null) { - ret = candidate.getReturn(); - this.releaseLock(); - return ret; - } - - this.releaseLock(); - return 258; + int ret = candidate.getReturn(); + this.releaseLock(pid); + return ret; } /* @@ -170,10 +141,10 @@ class ProcessManager { * or returns description of error */ public String getProcessOutput(int pid) { - String output; - try { - this.getLock(); + if(!this.getLock(pid)) { + return "[-] SERVER: Process not found"; + } } catch (TimeoutException e) { System.err.println("Timeout getting process output: " + e.getMessage()); @@ -181,14 +152,9 @@ class ProcessManager { } ProcessController candidate = this.processMap.get(pid); - if (candidate != null) { - output = candidate.getOutput(); - this.releaseLock(); - return output; - } - - this.releaseLock(); - return "[-] ERROR: Process not found"; + String output = candidate.getOutput(); + this.releaseLock(pid); + return output; } /* @@ -200,9 +166,10 @@ class ProcessManager { * returns 3 if couldnt grab lock */ public int killProcess(int pid) { - int code; try { - this.getLock(); + if(!this.getLock(pid)) { + return 2; + } } catch (TimeoutException e) { System.err.println("Timeout killing process: " + e.getMessage()); @@ -210,16 +177,9 @@ class ProcessManager { } ProcessController candidate = this.processMap.get(pid); - if (candidate != null) { - candidate.kill(); - code = 1; - - } else { - code = 2; - } - - this.releaseLock(); - return code; + candidate.kill(); + this.releaseLock(pid); + return 1; } /* @@ -228,25 +188,41 @@ class ProcessManager { * Waits for a predefined timeout period for mutex to be avail. * Synchronized so two things cannot grab lock at once. * Throws TimeoutException when it fails to get the lock. + * Alternatively, throws false if lock doesnt exist for PID + * Function is synchronized to prevent multiple threads accessing the same lock at once + * (ConcurrentHashMap will report whatever lock value was last to successfully update) */ - protected synchronized void getLock() throws TimeoutException { - Future future = this.threadPool.submit(this.lockCallable); + protected synchronized Boolean getLock(int pid) throws TimeoutException { + if (!lockMap.containsKey(pid)) { + return false; + } + + Future future = this.threadPool.submit( + new Callable() { + public Object call() { + while(lockMap.get(pid)) { + continue; // spin! + } + + lockMap.replace(pid, true); + return 1; + } + }); try { future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { System.err.println("[!] ERROR: " + e.getMessage()); - throw new TimeoutException(); - // rethrowing a timeout exception tells the calling process that they dont have the lock + future.cancel(true); + return false; } catch (ExecutionException e) { System.err.println("[!] ERROR: " + e.getMessage()); - throw new TimeoutException(); + future.cancel(true); + return false; // cancel the attempt to grab the lock - } finally { - future.cancel(true); } /* @@ -261,14 +237,16 @@ class ProcessManager { * mediate access to the ProcessManager * object for fresh calls as well. */ + + return true; } /* * releaseLock() * releases mutex so other threads can operate on processqueue */ - protected void releaseLock() { - this.processQueueMutex = false; + protected void releaseLock(int pid) { + this.lockMap.put(pid, false); } /* @@ -278,8 +256,6 @@ class ProcessManager { * releases resources held in the processController objects */ public void shutdown() { - this.processQueueMutex = true; - Iterator> iterator = this.processMap.entrySet().iterator(); while (iterator.hasNext()) { HashMap.Entry entry = iterator.next(); diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index 89b835a..d4104f1 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -31,10 +31,12 @@ public class ProcessManagerTest { private ProcessManagerTestImplementation manager = new ProcessManagerTestImplementation(); private ExecutorService threadPool = Executors.newCachedThreadPool(); + private int asyncTestPid; + // calls a test function that simulates load by holding the lock for a long time - private Callable holdLockSevenSeconds = new Callable() { + private Callable holdLockFourSeconds = new Callable() { public Object call() { - manager.longCallHoldsLock(); + manager.longCallHoldsLock(asyncTestPid); return true; } }; @@ -158,7 +160,7 @@ public class ProcessManagerTest { @Test public void getUnknownOutputTest() { String out = manager.getProcessOutput(532); - assertEquals("[-] ERROR: Process not found", out); + assertEquals("[-] SERVER: Process not found", out); manager.shutdown(); } @@ -187,22 +189,41 @@ public class ProcessManagerTest { */ @Test public void asyncLockTimeoutTest() { - Future future = this.threadPool.submit(this.holdLockSevenSeconds); + // start new process that will last the whole test + asyncTestPid = this.manager.newProcess("sleep 7"); + int secondProcess = this.manager.newProcess("sleep 10"); + + // grab that processes lock for 4 seconds + Future future = this.threadPool.submit(this.holdLockFourSeconds); try { - Thread.sleep(50); + Thread.sleep(100); } catch (InterruptedException e) { System.err.println("[!!] Thread for async test interrupted!"); } + // Try to grab a held lock System.err.println("[2] attempting to grab (held) lock"); - int pid = this.manager.newProcess("Test Process"); - assertEquals(-1, pid); + int status = this.manager.getProcessStatus(this.asyncTestPid); + assertEquals(4, status); // should time out after 2 secs - future.cancel(true); + // cancel the blocking thread, release lock + //future.cancel(true); - int pid2 = this.manager.newProcess("echo test"); - assertNotEquals(-1, pid2); + // try to grab unrelated lock (not nessesary, but important it works) + int statusTertiary = this.manager.getProcessStatus(secondProcess); + assertNotEquals(4, statusTertiary); + + // give lockMap small time to update + try { + Thread.sleep(100); + } catch (InterruptedException e) { + System.err.println("[!!] Thread for async test interrupted!"); + } + + // should be grabbable now + int statusSecondTry = this.manager.getProcessStatus(this.asyncTestPid); + assertNotEquals(4, statusSecondTry); manager.shutdown(); } diff --git a/src/test/java/JobServ/ProcessManagerTestImplementation.java b/src/test/java/JobServ/ProcessManagerTestImplementation.java index e9b44a7..9e879cb 100644 --- a/src/test/java/JobServ/ProcessManagerTestImplementation.java +++ b/src/test/java/JobServ/ProcessManagerTestImplementation.java @@ -17,25 +17,28 @@ import java.util.concurrent.TimeoutException; */ class ProcessManagerTestImplementation extends ProcessManager { - public void longCallHoldsLock() { + public void longCallHoldsLock(int pid) { try { - super.getLock(); + super.getLock(pid); System.err.println("[1] Long Call Has Lock"); // hold lock for 7 seconds, more than double normal timeout. Thread.sleep(4000); - super.releaseLock(); + super.releaseLock(pid); } catch (TimeoutException e) { System.err.println("[!!] Long Call wasnt able to grab lock!"); return; } catch (InterruptedException e) { - super.releaseLock(); + super.releaseLock(pid); // this doesnt happen, dont cancel this task System.err.println("[3] Released lock: interrupted"); return; } } + public Boolean reportLockState(int pid) { + return super.lockMap.get(pid); + } } From 88e745346ec8bd0a4d6225873a8249722644082a Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 22:38:39 -0700 Subject: [PATCH 53/63] removed superfluous return code from spec --- src/main/java/JobServ/JobServClient.java | 6 +----- src/main/java/JobServ/JobServClientAPIConnector.java | 5 ++--- src/main/java/JobServ/ProcessManager.java | 6 +++--- src/test/java/JobServ/ProcessManagerTest.java | 2 +- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index f348e82..0a041ac 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -101,14 +101,10 @@ public class JobServClient { int newProcess = this.api.sendNewJobMessage(command); switch(newProcess) { case -1: - System.out.println(this.serversideTimeoutErrorMessage); + System.out.println("Server failed to spawn process. Bad command."); break; case -2: - System.out.println("Server failed to create job, check server logs."); - break; - - case -3: // error logged by API Connector break; diff --git a/src/main/java/JobServ/JobServClientAPIConnector.java b/src/main/java/JobServ/JobServClientAPIConnector.java index b8f39c3..b8d50ef 100644 --- a/src/main/java/JobServ/JobServClientAPIConnector.java +++ b/src/main/java/JobServ/JobServClientAPIConnector.java @@ -87,9 +87,8 @@ class JobServClientAPIConnector { * sendNewJobMessage() * sends a shell command to the api server * returns new pid of job - * or -1 if server thread couldnt synchronize before timeout - * or -2 if server failed to create job - * or -3 if client fails to connect + * or -1 if server failed to create job + * or -2 if client fails to connect */ public int sendNewJobMessage(String command) { // thought of escaping this, but the vulnerability is only client side, from client user input. diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index dd955a1..ebac0b8 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -63,13 +63,13 @@ class ProcessManager { /* * newProcess() * Takes a command and returns the translated pid of a new process - * Returns -1 if getLock fails TODO: REMOVE - * Returns -2 if controller throws an IOException + * Returns -1 if controller throws an IOException */ public int newProcess(String command) { try { ProcessController newProc = new ProcessController(command); + // we dont need to lock the map yet this.lockMap.put(newProc.getPid(), true); this.processMap.put(newProc.getPid(), newProc); @@ -78,7 +78,7 @@ class ProcessManager { } catch (IOException e) { System.err.println("Couldnt Spawn New Command: " + e.getMessage()); - return -2; + return -1; } } diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index d4104f1..c0bc851 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -48,7 +48,7 @@ public class ProcessManagerTest { @Test public void addProcessesTest() { int pid = manager.newProcess("sleep 1"); - assertNotEquals(-2, pid); + assertNotEquals(-1, pid); manager.shutdown(); } From 55b77789c34bcd3f5e3f4efa327f5fdd4592d0a5 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 23:29:41 -0700 Subject: [PATCH 54/63] UX tweaks, better output request behaviour --- src/main/java/JobServ/JobServClient.java | 23 ++++---- .../JobServ/JobServClientAPIConnector.java | 5 +- src/main/java/JobServ/ProcessController.java | 52 +++++++++++-------- src/main/java/JobServ/ProcessManager.java | 4 +- src/main/java/JobServ/ShellServerService.java | 10 ++-- src/main/proto/jobserv.proto | 7 ++- src/test/java/JobServ/ProcessManagerTest.java | 4 +- 7 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 0a041ac..2939c5d 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -54,8 +54,8 @@ public class JobServClient { try { return Integer.parseInt(this.programArgs[index]); - } catch (InputMismatchException e) { - System.out.println(this.programArgs[index] + " is not a valid int, much less a valid pid"); + } catch (NumberFormatException e) { + System.out.println(this.programArgs[index] + " is not a valid integer"); return -1; } @@ -69,9 +69,9 @@ public class JobServClient { System.out.println("... new (command)\n"+ "Starts a new process on the server\n"+ "example: ./client key.pem cert.crt ca.crt localhost 8448 new echo hello world!\n\n"+ - "... output (pid)\n"+ - "Garners output from process on server\n"+ - "example: ./client key.pem cert.crt ca.crt localhost 8448 output 0\n\n"+ + "... output (pid) (lines)\n"+ + "Garners (lines) lines of output from process (pid) on server\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 output 0 5\n\n"+ "... status (pid)\n"+ "Returns whether process on server is running"+ "example: ./client key.pem cert.crt ca.crt localhost 8448 status 0\n\n"+ @@ -88,11 +88,6 @@ public class JobServClient { * makes a new process */ public void makeNewProcess() { - if (this.programArgs.length < 6) { - System.out.println("Improper formatting, try client --help"); - return; - } - String command = ""; for (int token = 6; token < this.programArgs.length; token++) { command += " " + this.programArgs[token]; @@ -121,12 +116,18 @@ public class JobServClient { * gets output from a process */ public void getOutput() { + if (this.programArgs.length < 8) { + System.out.println("Improper formatting, need a lines and a pid argument."); + return; + } + int candidatePid = this.getPidArg(6); + int lines = this.getPidArg(7); if (candidatePid < 0) { return; } - String processOutput = this.api.getProcessOutput(candidatePid); + String processOutput = this.api.getProcessOutput(candidatePid, lines); System.out.println(processOutput); } diff --git a/src/main/java/JobServ/JobServClientAPIConnector.java b/src/main/java/JobServ/JobServClientAPIConnector.java index b8d50ef..2e4a02c 100644 --- a/src/main/java/JobServ/JobServClientAPIConnector.java +++ b/src/main/java/JobServ/JobServClientAPIConnector.java @@ -63,11 +63,12 @@ class JobServClientAPIConnector { * sends the server a request for output from the process identified by 'pid' * returns process output as string */ - public String getProcessOutput(int pid) { + public String getProcessOutput(int pid, int lines) { logger.info("[+] requesting output"); - PIDMessage request = PIDMessage.newBuilder() + OutputRequestMessage request = OutputRequestMessage.newBuilder() .setPid(pid) + .setLines(lines) .build(); OutputMessage response; diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index 69c3bbf..ce2d599 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -8,10 +8,11 @@ package JobServ; -import java.util.Scanner; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.BufferedReader; /* * ProcessController @@ -28,7 +29,8 @@ class ProcessController { // interactive processes (out of scope for initial API) private OutputStream output; private InputStream input; - private Scanner outputScanner; + private InputStreamReader inputIntermediateStream; + private BufferedReader reader; private Process process; @@ -46,8 +48,8 @@ class ProcessController { this.process = Runtime.getRuntime().exec(command); this.output = this.process.getOutputStream(); this.input = this.process.getInputStream(); - this.outputScanner = new Scanner(this.input); - this.outputScanner.useDelimiter("\\A"); + this.inputIntermediateStream = new InputStreamReader(this.input); + this.reader = new BufferedReader(this.inputIntermediateStream); } /* @@ -61,9 +63,6 @@ class ProcessController { /* * getStatus() * returns whether or not the process is running - * this isnt a very direct way of getting the information - * The alternative is to use reflection to get into the private UNIXProcess class - * for the PID and to check that against 'ps' or a similar command * * TODO: (for future release) return thread state */ @@ -85,7 +84,7 @@ class ProcessController { * returns the exit code of the process * 256 if process is still running * 257 if process was killed manually and no longer exists - * (unix/posix defines an exit code as a uint8, so 256 is fair game) + * (unix/posix defines an exit code as a uint8, so 256+ is fair game) */ public int getReturn() { if (this.killedManually) { @@ -103,17 +102,23 @@ class ProcessController { * getOutput() * gets output from process */ - public String getOutput() { - if (this.killedManually) { - return ""; + public String getOutput(int lines) { + String output = ""; + for (int i = 0; i < lines; i++) { + String newLine = null; + try { + newLine = reader.readLine(); + } catch (IOException e) { + newLine = "[-] SERVER: error reading process output: " + e.getMessage(); + } finally { + if (newLine != null) { + output += newLine + "\n"; + } + } + } - String out = ""; - while(outputScanner.hasNext()) { - out += outputScanner.next(); - } - - return out; + return output; } /* @@ -121,15 +126,20 @@ class ProcessController { * Cleans up resources and destroys process */ public void kill() { + if (this.killedManually) { + System.err.println("[~] Tried to kill already killed process"); + return; + } + try { this.input.close(); this.output.close(); - this.outputScanner.close(); + this.inputIntermediateStream.close(); + this.reader.close(); this.process.destroy(); + this.killedManually = true; } catch (IOException e) { - // streams already closed + System.err.println("[-] Killing process failed: " + e.getMessage()); } - - this.killedManually = true; } } diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index ebac0b8..c11650d 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -140,7 +140,7 @@ class ProcessManager { * returns output of process 'pid' * or returns description of error */ - public String getProcessOutput(int pid) { + public String getProcessOutput(int pid, int lines) { try { if(!this.getLock(pid)) { return "[-] SERVER: Process not found"; @@ -152,7 +152,7 @@ class ProcessManager { } ProcessController candidate = this.processMap.get(pid); - String output = candidate.getOutput(); + String output = candidate.getOutput(lines); this.releaseLock(pid); return output; } diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java index 953b0e1..370557b 100644 --- a/src/main/java/JobServ/ShellServerService.java +++ b/src/main/java/JobServ/ShellServerService.java @@ -47,10 +47,11 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { * implements api endpoint as defined in jobserv.proto */ @Override - public void getOutput(PIDMessage request, + public void getOutput(OutputRequestMessage request, StreamObserver responder) { - String output = manager.getProcessOutput(request.getPid()); + String output = manager.getProcessOutput(request.getPid(), + request.getLines()); OutputMessage reply = OutputMessage.newBuilder() .setOutput(output) @@ -67,7 +68,8 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void newJob(NewJobMessage request, StreamObserver responder) { - int newPid = manager.newProcess(request.getCommand()); + String command = request.getCommand(); + int newPid = manager.newProcess(command); PIDMessage reply = PIDMessage.newBuilder() .setPid(newPid) @@ -101,7 +103,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void killJob(PIDMessage request, StreamObserver responder) { - int status = manager.getProcessStatus(request.getPid()); + int status = manager.killProcess(request.getPid()); StatusMessage reply = StatusMessage.newBuilder() .setProcessStatus(status) diff --git a/src/main/proto/jobserv.proto b/src/main/proto/jobserv.proto index 351710d..9f1812b 100644 --- a/src/main/proto/jobserv.proto +++ b/src/main/proto/jobserv.proto @@ -10,7 +10,7 @@ package JobServ; service ShellServer { rpc getStatus (PIDMessage) returns (StatusMessage) {} rpc getReturn (PIDMessage) returns (ReturnMessage) {} - rpc getOutput (PIDMessage) returns (OutputMessage) {} + rpc getOutput (OutputRequestMessage) returns (OutputMessage) {} rpc killJob (PIDMessage) returns (StatusMessage) {} rpc newJob (NewJobMessage) returns (PIDMessage) {} } @@ -23,6 +23,11 @@ message ReturnMessage { int32 ProcessReturnCode = 1; } +message OutputRequestMessage { + int32 Pid = 1; + int32 Lines = 2; +} + message OutputMessage { string Output = 1; } diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index c0bc851..9071146 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -145,7 +145,7 @@ public class ProcessManagerTest { // } - String out = manager.getProcessOutput(pid); + String out = manager.getProcessOutput(pid, 2); assertEquals("test\n", out); // calls string.equals() manager.shutdown(); @@ -159,7 +159,7 @@ public class ProcessManagerTest { */ @Test public void getUnknownOutputTest() { - String out = manager.getProcessOutput(532); + String out = manager.getProcessOutput(532, 10); assertEquals("[-] SERVER: Process not found", out); manager.shutdown(); } From 269e944252857292920d211cb02958924d5b2dff Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Thu, 23 May 2019 23:59:20 -0700 Subject: [PATCH 55/63] dont assume resources dir already exists. --- README.md | 2 +- buildwrapper.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1797e69..e3e2203 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ At this point you can copy the staging/client or staging/server folders to any e # Testing Running the gradle test task, or the buildwrapper will run all junit tests. -Currently that includes a test of certificate based authentication (Mutual TLS), tests for the thread safe process control module, and tests ensureing that only one thread can edit process information at a time. +Currently that includes a test of certificate based authentication (Mutual TLS), tests for the thread safe process control module, and tests ensuring that only one connection can access a processes information at a time. diff --git a/buildwrapper.sh b/buildwrapper.sh index c391e4c..b27a86c 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -12,7 +12,8 @@ TEST_CN=localhost TEST_PATH=resources/test # refactor this to test for directory existanc -rm -rf resources/* +rm -rf resources +mkdir resources/ mkdir resources/client mkdir resources/server mkdir resources/test From c10988bfdede9c8569abf02595b544043d6f23ca Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 25 May 2019 21:49:58 -0700 Subject: [PATCH 56/63] notify user when asking for output from a dead process --- src/main/java/JobServ/ProcessController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index ce2d599..0dfbef4 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -103,6 +103,10 @@ class ProcessController { * gets output from process */ public String getOutput(int lines) { + if(this.killedManually) { + return "[-] SERVER: Process has already been killed by a JobServ client!"; + } + String output = ""; for (int i = 0; i < lines; i++) { String newLine = null; From 0f64d99b36caa251a3b727e12e53b51fef4fedb2 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sat, 25 May 2019 21:53:43 -0700 Subject: [PATCH 57/63] added example commands to readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3e2203..4f6cf1f 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,16 @@ After changing directory to the 'staging/client' folder or the 'staging/server' $ ./server (port) $ ./client (hostname) (port) (command) (arguments) ``` +For example: +``` +$ ./buildwrapper.sh + ..... +$ cd staging/server +$ ./server 8448 & +$ cd ../client +$ client localhost 8448 new ping archive.org +``` alternatively, for guidance: - ``` $ ./server $ ./client help From 8dcccc86baaed1e758bff3d6d739c04275e118f3 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 26 May 2019 21:26:29 -0700 Subject: [PATCH 58/63] added logging module to server --- src/main/java/JobServ/JobServServer.java | 8 +- src/main/java/JobServ/ProcessController.java | 5 +- src/main/java/JobServ/ProcessManager.java | 21 +++-- src/main/java/JobServ/SimpleLogger.java | 84 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/main/java/JobServ/SimpleLogger.java diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 51c226a..070e35f 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -29,7 +29,7 @@ import java.util.logging.Logger; * Which accepts incoming connections from client. */ public class JobServServer { - private static final Logger logger = Logger.getLogger(JobServServer.class.getName()); + public static SimpleLogger logger = new SimpleLogger("JobServ-Server-"); private Server server; private ProcessManager manager; @@ -54,12 +54,12 @@ public class JobServServer { private void start() throws IOException { // TODO: this should be passed in from a configuration manager server.start(); - logger.info("Server initialized!"); + logger.write("Server initialized!"); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { - logger.info("Shutting down server"); + logger.write("Shutting down server"); manager.shutdown(); JobServServer.this.stop(); } @@ -125,7 +125,7 @@ public class JobServServer { return; } - System.out.println("JobServ Server Initialized! Connect anytime..."); + JobServServer.logger.write("Initialized JobServ Server"); server.blockUntilShutdown(); } } diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index 0dfbef4..2216b76 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -131,7 +131,7 @@ class ProcessController { */ public void kill() { if (this.killedManually) { - System.err.println("[~] Tried to kill already killed process"); + JobServServer.logger.write("Tried to kill already killed process"); return; } @@ -143,7 +143,8 @@ class ProcessController { this.process.destroy(); this.killedManually = true; } catch (IOException e) { - System.err.println("[-] Killing process failed: " + e.getMessage()); + JobServServer.logger.write("Killing process " + + String.valueOf(this.pid) + " failed: " + e.getMessage()); } } } diff --git a/src/main/java/JobServ/ProcessManager.java b/src/main/java/JobServ/ProcessManager.java index c11650d..736fd21 100644 --- a/src/main/java/JobServ/ProcessManager.java +++ b/src/main/java/JobServ/ProcessManager.java @@ -77,7 +77,8 @@ class ProcessManager { return newProc.getPid(); } catch (IOException e) { - System.err.println("Couldnt Spawn New Command: " + e.getMessage()); + JobServServer.logger.write("Couldnt Spawn New Command: (" + + command + "): " + e.getMessage()); return -1; } } @@ -99,7 +100,8 @@ class ProcessManager { } catch (TimeoutException e) { // lock could not be grabbed before timeout - System.err.println("Timeout getting process status: " + e.getMessage()); + JobServServer.logger.write("Timeout getting process " + + String.valueOf(pid) + " status: " + e.getMessage()); return 4; } @@ -125,7 +127,8 @@ class ProcessManager { } } catch (TimeoutException e) { - System.err.println("Timeout getting process return: " + e.getMessage()); + JobServServer.logger.write("Timeout getting process " + + String.valueOf(pid) + " return: " + e.getMessage()); return 259; } @@ -147,7 +150,8 @@ class ProcessManager { } } catch (TimeoutException e) { - System.err.println("Timeout getting process output: " + e.getMessage()); + JobServServer.logger.write("Timeout getting process " + + String.valueOf(pid) + " output: " + e.getMessage()); return "[-] SERVER: Timeout grabbing lock to access process information"; } @@ -172,7 +176,8 @@ class ProcessManager { } } catch (TimeoutException e) { - System.err.println("Timeout killing process: " + e.getMessage()); + JobServServer.logger.write("Timeout killing process " + + String.valueOf(pid) + ": " + e.getMessage()); return 3; } @@ -213,12 +218,14 @@ class ProcessManager { future.get(this.LOCK_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { - System.err.println("[!] ERROR: " + e.getMessage()); + JobServServer.logger.write("[!] Couldnt get lock " + + String.valueOf(pid) + ": "+ e.getMessage()); future.cancel(true); return false; } catch (ExecutionException e) { - System.err.println("[!] ERROR: " + e.getMessage()); + JobServServer.logger.write("[!] Couldnt get lock " + + String.valueOf(pid) + ": "+ e.getMessage()); future.cancel(true); return false; diff --git a/src/main/java/JobServ/SimpleLogger.java b/src/main/java/JobServ/SimpleLogger.java new file mode 100644 index 0000000..13e5b7c --- /dev/null +++ b/src/main/java/JobServ/SimpleLogger.java @@ -0,0 +1,84 @@ +/* + * SimpleLogger + * + * v1.0 + * + * May 26, 2019 + */ + +package JobServ; + +import java.io.File; +import java.sql.Timestamp; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; + +/* + * SimpleLogger + * Automatically manages the creation of and output to a log file + * TODO: Log Levels, decorations for entries of different severity + */ +class SimpleLogger { + private static final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); + private Timestamp programStart; + private FileWriter logWriter; + private Boolean writable = true; + + /* + * Constructor + * Initializes timestamp and opens new file for logging + */ + public SimpleLogger(String filePrefix) { + this.programStart = new Timestamp(System.currentTimeMillis()); + File currentLog = new File(filePrefix + this.dateTimeFormat.format(this.programStart)); + + try{ + this.logWriter = new FileWriter(currentLog, true); + + } catch (IOException e) { + System.out.println("Error creating LogWriter!"); + this.writable = false; + } + + this.write(this.programStart.toString() + ": JobServ Logging Started"); + } + + /* + * write + * appends a line of information to the log + */ + public void write(String message) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + message = currentTime.toString() + "> " + message; + + if (this.writable) { + try { + this.logWriter.write(message); + + } catch (IOException e) { + System.out.println(e.getMessage()); + this.writable = false; + } + } + + System.out.println(message); + } + + /* + * shutdown() + * called on server exit, closes the FileWriter and frees its resources + */ + public void shutdown() { + Timestamp exitTime = new Timestamp(System.currentTimeMillis()); + this.write(exitTime.toString() + ": JobServ Logging Stopped"); + + try { + this.logWriter.close(); + + } catch (IOException e) { + // not sure what would be appropriate to do here + System.out.println(e.getMessage()); + } + } +} From 97e2310c224404603f41fc9b41820723ebe77a08 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 26 May 2019 21:33:40 -0700 Subject: [PATCH 59/63] fixed logging behaviour --- .gitignore | 3 +++ buildwrapper.sh | 3 +++ src/main/java/JobServ/SimpleLogger.java | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e441be8..b250fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ resources/* # Dont commit certs or compiled software staging/* + +# Test Logs +JobServ-Server-* diff --git a/buildwrapper.sh b/buildwrapper.sh index b27a86c..d3183f7 100755 --- a/buildwrapper.sh +++ b/buildwrapper.sh @@ -121,3 +121,6 @@ cat << EOF > staging/server/server ./JobServ/bin/jobserv-server \$1 server.crt private.pem ca.crt EOF chmod +x staging/server/server + +echo "[+] removing test logs" +rm JobServ-Server-* diff --git a/src/main/java/JobServ/SimpleLogger.java b/src/main/java/JobServ/SimpleLogger.java index 13e5b7c..a07e48b 100644 --- a/src/main/java/JobServ/SimpleLogger.java +++ b/src/main/java/JobServ/SimpleLogger.java @@ -54,7 +54,8 @@ class SimpleLogger { if (this.writable) { try { - this.logWriter.write(message); + this.logWriter.write(message + "\n"); + this.logWriter.flush(); } catch (IOException e) { System.out.println(e.getMessage()); From da4d92de60dbc8649b41275736a09b3e4e49a544 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 26 May 2019 22:00:52 -0700 Subject: [PATCH 60/63] log all connections --- src/main/java/JobServ/ProcessController.java | 2 ++ src/main/java/JobServ/ShellServerService.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/JobServ/ProcessController.java b/src/main/java/JobServ/ProcessController.java index 2216b76..71e6ec5 100644 --- a/src/main/java/JobServ/ProcessController.java +++ b/src/main/java/JobServ/ProcessController.java @@ -50,6 +50,8 @@ class ProcessController { this.input = this.process.getInputStream(); this.inputIntermediateStream = new InputStreamReader(this.input); this.reader = new BufferedReader(this.inputIntermediateStream); + + JobServServer.logger.write("Job " + String.valueOf(this.pid) + ": " + command); } /* diff --git a/src/main/java/JobServ/ShellServerService.java b/src/main/java/JobServ/ShellServerService.java index 370557b..4ebc3de 100644 --- a/src/main/java/JobServ/ShellServerService.java +++ b/src/main/java/JobServ/ShellServerService.java @@ -33,6 +33,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void getStatus(PIDMessage request, StreamObserver responder) { + JobServServer.logger.write("New status request for pid: " + String.valueOf(request.getPid())); int status = manager.getProcessStatus(request.getPid()); StatusMessage reply = StatusMessage.newBuilder() @@ -50,6 +51,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void getOutput(OutputRequestMessage request, StreamObserver responder) { + JobServServer.logger.write("New Output request for pid: " + String.valueOf(request.getPid())); String output = manager.getProcessOutput(request.getPid(), request.getLines()); @@ -69,6 +71,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { StreamObserver responder) { String command = request.getCommand(); + JobServServer.logger.write("New job request: " + command); int newPid = manager.newProcess(command); PIDMessage reply = PIDMessage.newBuilder() @@ -86,6 +89,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void getReturn(PIDMessage request, StreamObserver responder) { + JobServServer.logger.write("New request for return from job: " + String.valueOf(request.getPid())); int retVal = manager.getProcessReturn(request.getPid()); ReturnMessage reply = ReturnMessage.newBuilder() @@ -103,6 +107,7 @@ class ShellServerService extends ShellServerGrpc.ShellServerImplBase { public void killJob(PIDMessage request, StreamObserver responder) { + JobServServer.logger.write("New Request to kill job: " + String.valueOf(request.getPid())); int status = manager.killProcess(request.getPid()); StatusMessage reply = StatusMessage.newBuilder() From 84ad967a981e48b14d05cef1a749640c8eef0992 Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 26 May 2019 22:13:34 -0700 Subject: [PATCH 61/63] fixed help output --- src/main/java/JobServ/JobServClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/JobServ/JobServClient.java b/src/main/java/JobServ/JobServClient.java index 2939c5d..3c66e53 100644 --- a/src/main/java/JobServ/JobServClient.java +++ b/src/main/java/JobServ/JobServClient.java @@ -73,14 +73,14 @@ public class JobServClient { "Garners (lines) lines of output from process (pid) on server\n"+ "example: ./client key.pem cert.crt ca.crt localhost 8448 output 0 5\n\n"+ "... status (pid)\n"+ - "Returns whether process on server is running"+ + "Returns whether process on server is running\n"+ "example: ./client key.pem cert.crt ca.crt localhost 8448 status 0\n\n"+ "... return (pid)\n"+ "Collects return code from remote process\n"+ "example: ./client key.pem cert.crt ca.crt localhost 8448 return 0\n\n"+ - "... kill (pid)"+ - "Immediately destroys remote process"+ - "example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0\n\n"); + "... kill (pid)\n"+ + "Immediately destroys remote process\n"+ + "example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0"); } /* From e7059a36c6d46ea519f7d6a71d9e0dcee6bf46bd Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Sun, 26 May 2019 22:26:11 -0700 Subject: [PATCH 62/63] lock map needs more time to update --- src/test/java/JobServ/ProcessManagerTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/JobServ/ProcessManagerTest.java b/src/test/java/JobServ/ProcessManagerTest.java index 9071146..5a1e3a7 100644 --- a/src/test/java/JobServ/ProcessManagerTest.java +++ b/src/test/java/JobServ/ProcessManagerTest.java @@ -207,16 +207,13 @@ public class ProcessManagerTest { int status = this.manager.getProcessStatus(this.asyncTestPid); assertEquals(4, status); // should time out after 2 secs - // cancel the blocking thread, release lock - //future.cancel(true); - // try to grab unrelated lock (not nessesary, but important it works) int statusTertiary = this.manager.getProcessStatus(secondProcess); assertNotEquals(4, statusTertiary); // give lockMap small time to update try { - Thread.sleep(100); + Thread.sleep(200); } catch (InterruptedException e) { System.err.println("[!!] Thread for async test interrupted!"); } From 32499676d9856a7b81fe1cc4fb518b49a7e8b72f Mon Sep 17 00:00:00 2001 From: Aidan Hahn Date: Mon, 27 May 2019 09:57:19 -0700 Subject: [PATCH 63/63] remember to shutdown logger, finally fix that pesky test --- src/main/java/JobServ/JobServServer.java | 3 ++- src/test/java/JobServ/ProcessManagerTestImplementation.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/JobServ/JobServServer.java b/src/main/java/JobServ/JobServServer.java index 070e35f..94649c8 100644 --- a/src/main/java/JobServ/JobServServer.java +++ b/src/main/java/JobServ/JobServServer.java @@ -60,7 +60,8 @@ public class JobServServer { @Override public void run() { logger.write("Shutting down server"); - manager.shutdown(); + logger.shutdown(); + manager.shutdown(); JobServServer.this.stop(); } }); diff --git a/src/test/java/JobServ/ProcessManagerTestImplementation.java b/src/test/java/JobServ/ProcessManagerTestImplementation.java index 9e879cb..58594b5 100644 --- a/src/test/java/JobServ/ProcessManagerTestImplementation.java +++ b/src/test/java/JobServ/ProcessManagerTestImplementation.java @@ -22,8 +22,8 @@ class ProcessManagerTestImplementation extends ProcessManager { super.getLock(pid); System.err.println("[1] Long Call Has Lock"); - // hold lock for 7 seconds, more than double normal timeout. - Thread.sleep(4000); + // hold lock for 3.5 seconds, more than double normal timeout. + Thread.sleep(3500); super.releaseLock(pid);