Merge branch 'betterScalingJobControll' into 'master'

Better scaling job control

See merge request whom/jobserv!3
This commit is contained in:
Aidan Hahn 2019-05-31 18:54:04 +00:00
commit 8c03a32fc4
19 changed files with 2286 additions and 3 deletions

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build
# Ignore emacs swapfiles
\#*
.\#*
# Ignore vim swapfiles
*\~
# Dont commit certs
resources/*
# Dont commit certs or compiled software
staging/*
# Test Logs
JobServ-Server-*

View file

@ -1,10 +1,49 @@
# JobServ # JobServ
Remote Procedure Calls over the protobuf API Remote Procedure Calls over the protobuf API
# Dependancies # Requirements
- openssl
- tar
# Building # Building
Gradle will manage dependencies, generate code, compile the java, and package the code.
# Testing 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 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 # 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:
```
$ ./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
```
# 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), tests for the thread safe process control module, and tests ensuring that only one connection can access a processes information at a time.

111
build.gradle Normal file
View file

@ -0,0 +1,111 @@
/*
* 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 'com.google.protobuf' version '0.8.8'
id 'application'
id 'com.adarshr.test-logger' version '1.6.0'
}
def grpcVersion = '1.20.0'
repositories {
//maven{ url "https://maven-central.storage-download.googleapis.com/repos/central/data/" }
//mavenLocal()
mavenCentral()
}
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 "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'
// grpc stuff
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'
}
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'
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.7.1"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
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
}

126
buildwrapper.sh Executable file
View file

@ -0,0 +1,126 @@
#!/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_PATH=resources/server
CLIENT_CA_CN=jobserv-client-ca
CLIENT_PATH=resources/client
TEST_CA_CN=jobserv-bad-cert-ca
TEST_CN=localhost
TEST_PATH=resources/test
# refactor this to test for directory existanc
rm -rf resources
mkdir resources/
mkdir resources/client
mkdir resources/server
mkdir resources/test
rm -rf staging
# 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=${TEST_CA_CN}"
# 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} -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"
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} -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"
openssl rsa -passin pass:${CLTCAPASS} -in $CLIENT_PATH/private.key -out $CLIENT_PATH/private.key
# Generate Test Key, Signing request, cert
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=${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"
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 "[+] 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/server/JobServ/bin/jobserv-client staging/server/JobServ/bin/jobserv-client.bat
echo "[+] populating certificates"
cp resources/server/server.crt staging/server/
cp resources/server/private.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/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
./JobServ/bin/jobserv-client private.pem client.crt ca.crt \$@
EOF
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
echo "[+] removing test logs"
rm JobServ-Server-*

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
gradlew vendored Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
# 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" "$@"

84
gradlew.bat vendored Normal file
View file

@ -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

View file

@ -0,0 +1,296 @@
/*
* JobServClient
*
* v1.0
*
* May 18, 2019
*/
package JobServ;
import io.grpc.netty.GrpcSslContexts;
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 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.";
private JobServClientAPIConnector api;
private String[] programArgs;
/*
* Constructor
* takes program arguments and an api connector object
*/
public JobServClient(String[] args, JobServClientAPIConnector api) {
this.programArgs = args;
this.api = api;
}
/*
* getPidArg()
* reentrant code was found in all commands except newjob
* this function pulls the pid argument and wraps around the integer cast
* returns -1 (an invalid PID) if bad index or unparsable int
*/
private int getPidArg(int index) {
if (this.programArgs.length < index) {
System.out.println("Improper formatting, try client --help");
return -1;
}
try {
return Integer.parseInt(this.programArgs[index]);
} catch (NumberFormatException e) {
System.out.println(this.programArgs[index] + " is not a valid integer");
return -1;
}
}
/*
* outputHelp()
* writes help information about all commands in the shell to screen
*/
public static void outputHelp() {
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) (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\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)\n"+
"Immediately destroys remote process\n"+
"example: ./client key.pem cert.crt ca.crt localhost 8448 kill 0");
}
/*
* makeNewProcess
* makes a new process
*/
public void makeNewProcess() {
String command = "";
for (int token = 6; token < this.programArgs.length; token++) {
command += " " + this.programArgs[token];
}
int newProcess = this.api.sendNewJobMessage(command);
switch(newProcess) {
case -1:
System.out.println("Server failed to spawn process. Bad command.");
break;
case -2:
// error logged by API Connector
break;
default:
System.out.printf("Process started, assigned pid is %d\n", newProcess);
break;
}
return;
}
/*
* getOutput
* 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, lines);
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;
}
}
}

View file

@ -0,0 +1,200 @@
/*
* JobServClientAPIConnector
*
* v1.0
*
* May 23, 2019
*/
package JobServ;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
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 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, int lines) {
logger.info("[+] requesting output");
OutputRequestMessage request = OutputRequestMessage.newBuilder()
.setPid(pid)
.setLines(lines)
.build();
OutputMessage response;
try {
// blocking network operation
response = blockingStub.getOutput(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, this.apiFailureMessage + ": " + e.getStatus());
return "<Error connecting to API>";
}
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 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();
}
}

View file

@ -0,0 +1,132 @@
/*
* JobServServer
*
* v1.0
*
* May 18, 2019
*/
package JobServ;
import io.grpc.Server;
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.util.InputMismatchException;
import java.io.File;
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 {
public static SimpleLogger logger = new SimpleLogger("JobServ-Server-");
private Server server;
private ProcessManager manager;
/*
* Constructor
* 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)
.build()
.start();
}
/*
* start()
* this initializes the server
*/
private void start() throws IOException {
// TODO: this should be passed in from a configuration manager
server.start();
logger.write("Server initialized!");
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
logger.write("Shutting down server");
logger.shutdown();
manager.shutdown();
JobServServer.this.stop();
}
});
}
/*
* stop()
* This is called when ctrl+c is pressed
*/
private void stop() {
if (server != null) {
server.shutdown();
}
}
/*
* 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()
* Entrypoint of hte server
* parses args and initializes a server object.
* calls server main loop.
*/
public static void main(String[] args) throws IOException, InterruptedException {
// 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 {
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!");
return;
} catch (SSLException e) {
System.out.println(e.getMessage());
return;
} catch (IOException e) {
System.out.println(e.getMessage());
return;
}
JobServServer.logger.write("Initialized JobServ Server");
server.blockUntilShutdown();
}
}

View file

@ -0,0 +1,152 @@
/*
* ProcessController
*
* v1.0
*
* May 22, 2019
*/
package JobServ;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;
/*
* 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 InputStreamReader inputIntermediateStream;
private BufferedReader reader;
private Process process;
private Boolean killedManually = false;
/*
* 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.getRuntime().exec(command);
this.output = this.process.getOutputStream();
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);
}
/*
* getPid()
* returns translated pid of this process
*/
public int getPid() {
return this.pid;
}
/*
* getStatus()
* returns whether or not the process is running
*
* TODO: (for future release) return thread state
*/
public int getStatus() {
if (this.killedManually) {
return 2;
}
try {
process.exitValue();
return 1;
} catch (IllegalThreadStateException e) {
return 0;
}
}
/*
* getReturn()
* 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)
*/
public int getReturn() {
if (this.killedManually) {
return 257;
}
try {
return process.exitValue();
} catch (IllegalThreadStateException e) {
return 256;
}
}
/*
* getOutput()
* 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;
try {
newLine = reader.readLine();
} catch (IOException e) {
newLine = "[-] SERVER: error reading process output: " + e.getMessage();
} finally {
if (newLine != null) {
output += newLine + "\n";
}
}
}
return output;
}
/*
* kill()
* Cleans up resources and destroys process
*/
public void kill() {
if (this.killedManually) {
JobServServer.logger.write("Tried to kill already killed process");
return;
}
try {
this.input.close();
this.output.close();
this.inputIntermediateStream.close();
this.reader.close();
this.process.destroy();
this.killedManually = true;
} catch (IOException e) {
JobServServer.logger.write("Killing process " +
String.valueOf(this.pid) + " failed: " + e.getMessage());
}
}
}

View file

@ -0,0 +1,274 @@
/*
* ProcessManager
*
* v1.0
*
* May 22, 2019
*/
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.ConcurrentHashMap;
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
* 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 = 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<Integer, ProcessController> processMap;
protected ConcurrentHashMap<Integer, Boolean> lockMap;
private ExecutorService threadPool = Executors.newCachedThreadPool();
/*
* Constructor
* initializes process queue and start the background process checking daemon
*/
public ProcessManager() {
processMap = new ConcurrentHashMap<Integer, ProcessController>();
lockMap = new ConcurrentHashMap<Integer, Boolean>();
/* 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 finished processes, store exit codes, release lock, sleep, repeat)
*/
}
/*
* newProcess()
* Takes a command and returns the translated pid of a new process
* 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);
this.releaseLock(newProc.getPid());
return newProc.getPid();
} catch (IOException e) {
JobServServer.logger.write("Couldnt Spawn New Command: (" +
command + "): " + e.getMessage());
return -1;
}
}
/*
* getProcessStatus()
* returns whether or not a process is running.
* 0: running
* 1: not running
* 2: killed manually by a client
* 3: doesnt exist
* 4: couldnt grab lock
*/
public int getProcessStatus(int pid) {
try {
if(!this.getLock(pid)) {
return 3;
}
} catch (TimeoutException e) {
// lock could not be grabbed before timeout
JobServServer.logger.write("Timeout getting process " +
String.valueOf(pid) + " status: " + e.getMessage());
return 4;
}
ProcessController candidate = this.processMap.get(pid);
int status = candidate.getStatus();
this.releaseLock(pid);
return status;
}
/*
* getProcessReturn()
* 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) {
try {
if(!this.getLock(pid)) {
return 258;
}
} catch (TimeoutException e) {
JobServServer.logger.write("Timeout getting process " +
String.valueOf(pid) + " return: " + e.getMessage());
return 259;
}
ProcessController candidate = this.processMap.get(pid);
int ret = candidate.getReturn();
this.releaseLock(pid);
return ret;
}
/*
* getProcessOutput()
* returns output of process 'pid'
* or returns description of error
*/
public String getProcessOutput(int pid, int lines) {
try {
if(!this.getLock(pid)) {
return "[-] SERVER: Process not found";
}
} catch (TimeoutException e) {
JobServServer.logger.write("Timeout getting process " +
String.valueOf(pid) + " output: " + e.getMessage());
return "[-] SERVER: Timeout grabbing lock to access process information";
}
ProcessController candidate = this.processMap.get(pid);
String output = candidate.getOutput(lines);
this.releaseLock(pid);
return output;
}
/*
* killProcess()
* 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) {
try {
if(!this.getLock(pid)) {
return 2;
}
} catch (TimeoutException e) {
JobServServer.logger.write("Timeout killing process " +
String.valueOf(pid) + ": " + e.getMessage());
return 3;
}
ProcessController candidate = this.processMap.get(pid);
candidate.kill();
this.releaseLock(pid);
return 1;
}
/*
* getLock()
* Locks access to this.processQueue
* 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 Boolean getLock(int pid) throws TimeoutException {
if (!lockMap.containsKey(pid)) {
return false;
}
Future<Object> future = this.threadPool.submit(
new Callable<Object>() {
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) {
JobServServer.logger.write("[!] Couldnt get lock " +
String.valueOf(pid) + ": "+ e.getMessage());
future.cancel(true);
return false;
} catch (ExecutionException e) {
JobServServer.logger.write("[!] Couldnt get lock " +
String.valueOf(pid) + ": "+ e.getMessage());
future.cancel(true);
return false;
// cancel the attempt to grab the lock
}
/*
* 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.
*/
return true;
}
/*
* releaseLock()
* releases mutex so other threads can operate on processqueue
*/
protected void releaseLock(int pid) {
this.lockMap.put(pid, 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
*/
public void shutdown() {
Iterator<HashMap.Entry<Integer, ProcessController>> iterator = this.processMap.entrySet().iterator();
while (iterator.hasNext()) {
HashMap.Entry<Integer, ProcessController> entry = iterator.next();
entry.getValue().kill();
iterator.remove();
}
}
}

View file

@ -0,0 +1,119 @@
/*
* 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 {
private ProcessManager manager;
/*
* constructor
* initialized ProcessManager
*/
public ShellServerService(ProcessManager manager) {
this.manager = manager;
}
/*
* getStatus
* implements api endpoint as defined in jobserv.proto
*/
@Override
public void getStatus(PIDMessage request,
StreamObserver<StatusMessage> responder) {
JobServServer.logger.write("New status request for pid: " + String.valueOf(request.getPid()));
int status = manager.getProcessStatus(request.getPid());
StatusMessage reply = StatusMessage.newBuilder()
.setProcessStatus(status)
.build();
responder.onNext(reply);
responder.onCompleted();
}
/*
* getOutput
* implements api endpoint as defined in jobserv.proto
*/
@Override
public void getOutput(OutputRequestMessage request,
StreamObserver<OutputMessage> responder) {
JobServServer.logger.write("New Output request for pid: " + String.valueOf(request.getPid()));
String output = manager.getProcessOutput(request.getPid(),
request.getLines());
OutputMessage reply = OutputMessage.newBuilder()
.setOutput(output)
.build();
responder.onNext(reply);
responder.onCompleted();
}
/*
* newJob
* implements api endpoint as defined in jobserv.proto
*/
@Override
public void newJob(NewJobMessage request,
StreamObserver<PIDMessage> responder) {
String command = request.getCommand();
JobServServer.logger.write("New job request: " + command);
int newPid = manager.newProcess(command);
PIDMessage reply = PIDMessage.newBuilder()
.setPid(newPid)
.build();
responder.onNext(reply);
responder.onCompleted();
}
/*
* getReturn
* implements api endpoint as defined in jobserv.proto
*/
@Override
public void getReturn(PIDMessage request,
StreamObserver<ReturnMessage> responder) {
JobServServer.logger.write("New request for return from job: " + String.valueOf(request.getPid()));
int retVal = manager.getProcessReturn(request.getPid());
ReturnMessage reply = ReturnMessage.newBuilder()
.setProcessReturnCode(retVal)
.build();
responder.onNext(reply);
responder.onCompleted();
}
/*
* killJob
* implements api endpoint as defined in jobserv.proto
*/
@Override
public void killJob(PIDMessage request,
StreamObserver<StatusMessage> responder) {
JobServServer.logger.write("New Request to kill job: " + String.valueOf(request.getPid()));
int status = manager.killProcess(request.getPid());
StatusMessage reply = StatusMessage.newBuilder()
.setProcessStatus(status)
.build();
responder.onNext(reply);
responder.onCompleted();
}
}

View file

@ -0,0 +1,85 @@
/*
* 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 + "\n");
this.logWriter.flush();
} 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());
}
}
}

View file

@ -0,0 +1,41 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "JobServ";
option java_outer_classname = "JobServGrpc";
option objc_class_prefix = "JSV";
package JobServ;
service ShellServer {
rpc getStatus (PIDMessage) returns (StatusMessage) {}
rpc getReturn (PIDMessage) returns (ReturnMessage) {}
rpc getOutput (OutputRequestMessage) returns (OutputMessage) {}
rpc killJob (PIDMessage) returns (StatusMessage) {}
rpc newJob (NewJobMessage) returns (PIDMessage) {}
}
message StatusMessage {
int32 ProcessStatus = 1;
}
message ReturnMessage {
int32 ProcessReturnCode = 1;
}
message OutputRequestMessage {
int32 Pid = 1;
int32 Lines = 2;
}
message OutputMessage {
string Output = 1;
}
message NewJobMessage {
string Command = 1;
}
message PIDMessage {
int32 Pid = 1;
}

View file

@ -0,0 +1,154 @@
/*
* JobServerAuthenticationTest
*
* v1.0
*
* May 21, 2019
*/
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;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import io.grpc.ManagedChannel;
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;
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.BeforeClass;
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 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";
private final String clientCert = projectRoot + "resources/client/client.crt";
// Authorized server key/cert/ca
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 = 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 JobServClientAPIConnector goodClient;
private JobServClientAPIConnector badClient;
private JobServServer server;
// 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());
}
// 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));
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 JobServClientAPIConnector(goodChannel);
badClient = new JobServClientAPIConnector(badChannel);
this.clientSslInitialized = true;
} catch (SSLException e) {
this.clientSslInitialized = false;
System.err.println(e.getMessage());
}
} else {
this.clientSslInitialized = false;
}
}
/*
* 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 certificateAuthenticationTest() {
assertEquals(true, serverSslInitialized);
assertEquals(true, clientSslInitialized);
int result = badClient.sendNewJobMessage("test command");
assertEquals(-3, result);
result = goodClient.sendNewJobMessage("test command");
Boolean assertCondition = result == -3;
assertEquals(assertCondition, false);
}
}

View file

@ -0,0 +1,228 @@
/*
* 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.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
* 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 {
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<Object> holdLockFourSeconds = new Callable<Object>() {
public Object call() {
manager.longCallHoldsLock(asyncTestPid);
return true;
}
};
/*
* addProcessTest()
* positive unit test for newProcess
*/
@Test
public void addProcessesTest() {
int pid = manager.newProcess("sleep 1");
assertNotEquals(-1, pid);
manager.shutdown();
}
/*
* getStatusTest
* unit test for getStatus
*/
@Test
public void getStatusTest() {
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(3, 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, 2);
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, 10);
assertEquals("[-] SERVER: Process not found", out);
manager.shutdown();
}
/*
* 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(1, ret);
int status = manager.getProcessStatus(pid);
assertEquals(2, status);
manager.shutdown();
}
/*
* asyncLockTimeoutTest
* ensures that two things cannot grab the lock at the same time
*/
@Test
public void asyncLockTimeoutTest() {
// 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<Object> future = this.threadPool.submit(this.holdLockFourSeconds);
try {
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 status = this.manager.getProcessStatus(this.asyncTestPid);
assertEquals(4, status); // should time out after 2 secs
// 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(200);
} 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();
}
}

View file

@ -0,0 +1,44 @@
/*
* 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(int pid) {
try {
super.getLock(pid);
System.err.println("[1] Long Call Has Lock");
// hold lock for 3.5 seconds, more than double normal timeout.
Thread.sleep(3500);
super.releaseLock(pid);
} catch (TimeoutException e) {
System.err.println("[!!] Long Call wasnt able to grab lock!");
return;
} catch (InterruptedException e) {
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);
}
}