Merge branch 'betterScalingJobControll' into 'master'
Better scaling job control See merge request whom/jobserv!3
This commit is contained in:
commit
8c03a32fc4
19 changed files with 2286 additions and 3 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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-*
|
||||
45
README.md
45
README.md
|
|
@ -1,10 +1,49 @@
|
|||
# JobServ
|
||||
Remote Procedure Calls over the protobuf API
|
||||
|
||||
# Dependancies
|
||||
# Requirements
|
||||
- openssl
|
||||
- tar
|
||||
|
||||
# Building
|
||||
|
||||
# Testing
|
||||
Gradle will manage dependencies, generate code, compile the java, and package the code.
|
||||
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
|
||||
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
111
build.gradle
Normal 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
126
buildwrapper.sh
Executable 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
172
gradlew
vendored
Executable 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
84
gradlew.bat
vendored
Normal 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
|
||||
296
src/main/java/JobServ/JobServClient.java
Normal file
296
src/main/java/JobServ/JobServClient.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/main/java/JobServ/JobServClientAPIConnector.java
Normal file
200
src/main/java/JobServ/JobServClientAPIConnector.java
Normal 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();
|
||||
}
|
||||
}
|
||||
132
src/main/java/JobServ/JobServServer.java
Normal file
132
src/main/java/JobServ/JobServServer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
152
src/main/java/JobServ/ProcessController.java
Normal file
152
src/main/java/JobServ/ProcessController.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
274
src/main/java/JobServ/ProcessManager.java
Normal file
274
src/main/java/JobServ/ProcessManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/main/java/JobServ/ShellServerService.java
Normal file
119
src/main/java/JobServ/ShellServerService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
85
src/main/java/JobServ/SimpleLogger.java
Normal file
85
src/main/java/JobServ/SimpleLogger.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/main/proto/jobserv.proto
Normal file
41
src/main/proto/jobserv.proto
Normal 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;
|
||||
}
|
||||
154
src/test/java/JobServ/JobServerAuthenticationTest.java
Normal file
154
src/test/java/JobServ/JobServerAuthenticationTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
228
src/test/java/JobServ/ProcessManagerTest.java
Normal file
228
src/test/java/JobServ/ProcessManagerTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
44
src/test/java/JobServ/ProcessManagerTestImplementation.java
Normal file
44
src/test/java/JobServ/ProcessManagerTestImplementation.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue