mirror of
https://github.com/LamGC/Oracle-Sentry.git
synced 2025-04-29 14:17:34 +00:00
[Initial] Initial Commit;
This commit is contained in:
commit
3dda31efb7
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
# test runs directory
|
||||||
|
/run/
|
||||||
|
|
||||||
|
# gradle
|
||||||
|
/.gradle
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Idea Editor
|
||||||
|
/.idea
|
46
build.gradle
Normal file
46
build.gradle
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '2.5.3'
|
||||||
|
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'net.lamgc.oracle'
|
||||||
|
version '0.0.1-SNAPSHOT'
|
||||||
|
compileJava.sourceCompatibility = '16'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
def ociSdkVer = '2.3.2'
|
||||||
|
def sshdVer = '2.7.0'
|
||||||
|
def bouncyCastleVer = '1.69'
|
||||||
|
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter'
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
|
||||||
|
implementation 'org.slf4j:slf4j-api:1.7.31'
|
||||||
|
// implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.1'
|
||||||
|
|
||||||
|
|
||||||
|
implementation "com.oracle.oci.sdk:oci-java-sdk-core:${ociSdkVer}"
|
||||||
|
implementation "com.oracle.oci.sdk:oci-java-sdk-identity:${ociSdkVer}"
|
||||||
|
|
||||||
|
implementation "org.apache.sshd:sshd-core:${sshdVer}"
|
||||||
|
implementation "org.apache.sshd:sshd-sftp:${sshdVer}"
|
||||||
|
|
||||||
|
implementation "org.bouncycastle:bcpg-jdk15on:${bouncyCastleVer}"
|
||||||
|
implementation "org.bouncycastle:bcpkix-jdk15on:${bouncyCastleVer}"
|
||||||
|
|
||||||
|
implementation 'org.codehaus.groovy:groovy-all:3.0.7'
|
||||||
|
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.7'
|
||||||
|
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
|
||||||
|
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
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-7.0-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
185
gradlew
vendored
Normal file
185
gradlew
vendored
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for 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" "-Xms64m"'
|
||||||
|
|
||||||
|
# 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 or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
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 execute
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="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
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = 'oracle-sentry'
|
||||||
|
|
101
src/main/java/net/lamgc/oracle/sentry/ApplicationMain.java
Normal file
101
src/main/java/net/lamgc/oracle/sentry/ApplicationMain.java
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package net.lamgc.oracle.sentry;
|
||||||
|
|
||||||
|
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptComponent;
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptManager;
|
||||||
|
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p> 程序入口
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
@Configuration
|
||||||
|
public class ApplicationMain {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(ApplicationMain.class);
|
||||||
|
|
||||||
|
@SuppressWarnings("AlibabaConstantFieldShouldBeUpperCase")
|
||||||
|
private final static Object mainThreadWaiter = new Object();
|
||||||
|
|
||||||
|
@Value("${oracle.identity.location:./identity/}")
|
||||||
|
private String identityDirectory;
|
||||||
|
@Value("${oracle.identity.pattern:.*\\.oracle.ini$}")
|
||||||
|
private String identityFilePattern;
|
||||||
|
@Value("${sentry.script.location:./scripts/}")
|
||||||
|
private String scriptsLocation;
|
||||||
|
@Value("${oracle.script.ssh.identityPath:./ssh.config.json}")
|
||||||
|
private String sshIdentityPath;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ApplicationMain.class, args);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(mainThreadWaiter::notifyAll, "ShutdownMainThread"));
|
||||||
|
synchronized (mainThreadWaiter) {
|
||||||
|
try {
|
||||||
|
mainThreadWaiter.wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.warn("", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("oracle.identity.manager")
|
||||||
|
public OracleIdentityManager initialOracleIdentityManager() throws IOException {
|
||||||
|
OracleIdentityManager oracleUserManager = new OracleIdentityManager();
|
||||||
|
log.info("正在加载 Oracle API 身份配置...");
|
||||||
|
log.debug("Oracle API 身份配置查找路径: \"{}\", 匹配表达式: {}", identityDirectory, identityFilePattern);
|
||||||
|
int loadedCount = oracleUserManager.loadFromDirectory(new File(identityDirectory), identityFilePattern);
|
||||||
|
log.info("已加载 {} 个身份配置.", loadedCount);
|
||||||
|
return oracleUserManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("oracle.compute.instance.manager")
|
||||||
|
@Autowired
|
||||||
|
public ComputeInstanceManager initialComputeInstanceManager(OracleIdentityManager identityManager) throws IOException {
|
||||||
|
ComputeInstanceManager instanceManager = new ComputeInstanceManager();
|
||||||
|
int addTotal = 0;
|
||||||
|
for (AuthenticationDetailsProvider provider : identityManager.getProviders()) {
|
||||||
|
String identityName = identityManager.getIdentityName(provider.getUserId());
|
||||||
|
log.info("正在加载用户 {} 所拥有的所有实例...", identityName);
|
||||||
|
int addCount;
|
||||||
|
try {
|
||||||
|
addCount = instanceManager.addComputeInstanceFromUser(provider);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("加载实例时发生异常.", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info("用户 {} 已添加 {} 个计算实例.", identityName, addCount);
|
||||||
|
addTotal += addCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("正在初始化 SSH 认证配置提供器...");
|
||||||
|
instanceManager.initialSshIdentityProvider(new File(sshIdentityPath));
|
||||||
|
log.info("已完成 ComputeInstanceManager 初始化, 共加载了 {} 个计算实例.", addTotal);
|
||||||
|
return instanceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("sentry.script.manager")
|
||||||
|
@Autowired
|
||||||
|
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager) {
|
||||||
|
ScriptComponent context = new ScriptComponent(new ScriptHttpClient(HttpClientBuilder.create()
|
||||||
|
.build()),
|
||||||
|
instanceManager);
|
||||||
|
|
||||||
|
ScriptManager manager = new ScriptManager(new File(scriptsLocation), context);
|
||||||
|
manager.loadScripts();
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
package net.lamgc.oracle.sentry;
|
||||||
|
|
||||||
|
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
|
||||||
|
import com.oracle.bmc.core.ComputeClient;
|
||||||
|
import com.oracle.bmc.core.model.Instance;
|
||||||
|
import com.oracle.bmc.core.requests.ListInstancesRequest;
|
||||||
|
import com.oracle.bmc.core.responses.ListInstancesResponse;
|
||||||
|
import com.oracle.bmc.identity.IdentityClient;
|
||||||
|
import com.oracle.bmc.identity.model.Compartment;
|
||||||
|
import com.oracle.bmc.identity.requests.ListCompartmentsRequest;
|
||||||
|
import com.oracle.bmc.identity.responses.ListCompartmentsResponse;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ssh.SshAuthIdentityProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算实例管理器.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class ComputeInstanceManager {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(ComputeInstanceManager.class);
|
||||||
|
|
||||||
|
private final Map<String, ComputeInstance> instanceMap = new ConcurrentHashMap<>();
|
||||||
|
private SshAuthIdentityProvider sshIdentityProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 SSH 认证配置提供器.
|
||||||
|
* @param sshIdentityJson SSH 认证配置文件.
|
||||||
|
* @throws IOException 加载时如有异常将直接抛出.
|
||||||
|
*/
|
||||||
|
public void initialSshIdentityProvider(File sshIdentityJson) throws IOException {
|
||||||
|
sshIdentityProvider = new SshAuthIdentityProvider(this, sshIdentityJson);
|
||||||
|
sshIdentityProvider.loadAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SshAuthIdentityProvider getSshIdentityProvider() {
|
||||||
|
return sshIdentityProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单独添加一个计算实例对象.
|
||||||
|
* @param instance 计算实例对象.
|
||||||
|
* @throws NullPointerException 当 instance 为 {@code null} 时抛出异常.
|
||||||
|
*/
|
||||||
|
public void addComputeInstance(ComputeInstance instance) {
|
||||||
|
Objects.requireNonNull(instance);
|
||||||
|
instanceMap.put(instance.getInstanceId(), instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某一用户的所有已添加实例.
|
||||||
|
* @param userId 用户 Id.
|
||||||
|
* @return 返回该用户所拥有的的所有已添加实例.
|
||||||
|
* @throws NullPointerException 当 userId 为 {@code null} 时抛出异常.
|
||||||
|
*/
|
||||||
|
public Set<ComputeInstance> getInstancesByUserId(String userId) {
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
return instanceMap.values().stream()
|
||||||
|
.filter(computeInstance -> computeInstance.getUserId().equals(userId))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加某一用户的所有计算实例.
|
||||||
|
* @param provider 用户身份提供器.
|
||||||
|
* @return 返回已成功添加的实例数量.
|
||||||
|
* @throws NullPointerException 如果 provider 为 {@code null} 则抛出异常.
|
||||||
|
*/
|
||||||
|
public int addComputeInstanceFromUser(AuthenticationDetailsProvider provider) {
|
||||||
|
Objects.requireNonNull(provider);
|
||||||
|
IdentityClient identityClient = new IdentityClient(provider);
|
||||||
|
ComputeClient computeClient = new ComputeClient(provider);
|
||||||
|
ListCompartmentsResponse listCompartments = identityClient.listCompartments(ListCompartmentsRequest.builder()
|
||||||
|
.compartmentId(provider.getTenantId())
|
||||||
|
.build());
|
||||||
|
int addCount = 0;
|
||||||
|
Set<String> compartmentIds = listCompartments.getItems().stream()
|
||||||
|
.map(Compartment::getId).collect(Collectors.toSet());
|
||||||
|
compartmentIds.add(provider.getTenantId());
|
||||||
|
for (String compartmentId : compartmentIds) {
|
||||||
|
ListInstancesResponse listInstances = computeClient.listInstances(ListInstancesRequest.builder()
|
||||||
|
.compartmentId(compartmentId)
|
||||||
|
.build());
|
||||||
|
for (Instance instance : listInstances.getItems()) {
|
||||||
|
ComputeInstance computeInstance = new ComputeInstance(this, instance.getId(),
|
||||||
|
provider.getUserId(), compartmentId, instance.getImageId(), provider);
|
||||||
|
|
||||||
|
addComputeInstance(computeInstance);
|
||||||
|
addCount ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过实例 Id 获取计算实例对象.
|
||||||
|
* @param instanceId 实例 Id.
|
||||||
|
* @return 返回计算实例对象.
|
||||||
|
* @throws NullPointerException 当 instanceId 为 {@code null} 时抛出异常.
|
||||||
|
* @throws NoSuchElementException 当未找到指定计算实例时抛出该异常.
|
||||||
|
*/
|
||||||
|
public ComputeInstance getComputeInstanceById(String instanceId) {
|
||||||
|
Objects.requireNonNull(instanceId);
|
||||||
|
if (!instanceMap.containsKey(instanceId)) {
|
||||||
|
throw new NoSuchElementException(instanceId);
|
||||||
|
}
|
||||||
|
return instanceMap.get(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有计算实例.
|
||||||
|
* @return 返回所有已添加的计算实例.
|
||||||
|
*/
|
||||||
|
public Set<ComputeInstance> getComputeInstances() {
|
||||||
|
return instanceMap.values().stream().collect(Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
189
src/main/java/net/lamgc/oracle/sentry/OracleIdentityManager.java
Normal file
189
src/main/java/net/lamgc/oracle/sentry/OracleIdentityManager.java
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package net.lamgc.oracle.sentry;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.oracle.bmc.ConfigFileReader;
|
||||||
|
import com.oracle.bmc.Region;
|
||||||
|
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
|
||||||
|
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
|
||||||
|
import com.oracle.bmc.auth.SimplePrivateKeySupplier;
|
||||||
|
import com.oracle.bmc.identity.IdentityClient;
|
||||||
|
import com.oracle.bmc.identity.requests.GetUserRequest;
|
||||||
|
import com.oracle.bmc.identity.responses.GetUserResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oracle 身份管理器.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final class OracleIdentityManager {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(OracleIdentityManager.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证身份 Map.
|
||||||
|
* Key: Identity Id
|
||||||
|
* Value {@link AuthenticationDetailsProvider}
|
||||||
|
*/
|
||||||
|
private final Map<String, AuthenticationDetailsProvider> identityMap = new ConcurrentHashMap<>();
|
||||||
|
/**
|
||||||
|
* 用户名 Map.
|
||||||
|
* key Identity Id
|
||||||
|
* Value: Username
|
||||||
|
*/
|
||||||
|
private final Map<String, String> identityNameMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从目录扫描匹配的配置文件并加载.
|
||||||
|
* @param directory 待扫描的目录.
|
||||||
|
* @param pattern 文件匹配规则(正则表达式).
|
||||||
|
* @throws IOException 当加载发生异常时将抛出该异常.
|
||||||
|
* @return 返回成功加载的身份配置数量.
|
||||||
|
*/
|
||||||
|
public int loadFromDirectory(File directory, String pattern) throws IOException {
|
||||||
|
if (!directory.exists()) {
|
||||||
|
throw new FileNotFoundException(directory.getCanonicalPath());
|
||||||
|
} else if (!directory.isDirectory()) {
|
||||||
|
throw new IOException("The specified path is not a folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
File[] configFiles = directory.listFiles(file -> file.isFile() && file.getName().matches(pattern));
|
||||||
|
if (configFiles == null) {
|
||||||
|
throw new IOException("Unable to access the specified directory: " + directory.getCanonicalPath());
|
||||||
|
}
|
||||||
|
int loadedCount = 0;
|
||||||
|
for (File configFile : configFiles) {
|
||||||
|
try {
|
||||||
|
AuthenticationDetailsProvider provider = loadFromConfigFile(configFile);
|
||||||
|
loadedCount ++;
|
||||||
|
log.info("已成功加载身份配置文件.\nUserId: {}\nUsername: {}\nPath: {})",
|
||||||
|
provider.getUserId(),
|
||||||
|
getIdentityName(provider.getUserId()),
|
||||||
|
configFile.getCanonicalPath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("加载身份配置文件时发生异常.(Path: {})\n{}", configFile.getCanonicalPath(), Throwables.getStackTraceAsString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loadedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过配置文件加载身份信息.
|
||||||
|
* @param identityConfig 身份信息文件.
|
||||||
|
* @throws IOException 如果读取文件发生问题时将抛出该异常.
|
||||||
|
*/
|
||||||
|
public AuthenticationDetailsProvider loadFromConfigFile(File identityConfig) throws IOException {
|
||||||
|
if (!identityConfig.exists()) {
|
||||||
|
throw new FileNotFoundException(identityConfig.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigFileReader.ConfigFile config
|
||||||
|
= ConfigFileReader.parse(identityConfig.getAbsolutePath());
|
||||||
|
|
||||||
|
String keyFilePath = config.get("key_file");
|
||||||
|
if (keyFilePath.startsWith(".")) {
|
||||||
|
keyFilePath = new File(identityConfig.getParent(), config.get("key_file")).getCanonicalPath();
|
||||||
|
}
|
||||||
|
Supplier<InputStream> privateKeySupplier
|
||||||
|
= new SimplePrivateKeySupplier(keyFilePath);
|
||||||
|
|
||||||
|
AuthenticationDetailsProvider provider
|
||||||
|
= SimpleAuthenticationDetailsProvider.builder()
|
||||||
|
.region(Region.fromRegionCode(config.get("region")))
|
||||||
|
.tenantId(config.get("tenancy"))
|
||||||
|
.userId(config.get("user"))
|
||||||
|
.fingerprint(config.get("fingerprint"))
|
||||||
|
.privateKeySupplier(privateKeySupplier::get)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 尝试获取身份所属用户名, 以此检查该身份配置是否正确.
|
||||||
|
String identityName = getIdentityName0(provider);
|
||||||
|
identityNameMap.put(provider.getUserId(), identityName);
|
||||||
|
identityMap.put(provider.getUserId(), provider);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取身份所属用户的名称.
|
||||||
|
* @param provider 身份提供器.
|
||||||
|
* @return 返回用户名.
|
||||||
|
*/
|
||||||
|
private String getIdentityName0(AuthenticationDetailsProvider provider) {
|
||||||
|
IdentityClient identityClient = new IdentityClient(provider);
|
||||||
|
GetUserResponse user = identityClient.getUser(GetUserRequest.builder()
|
||||||
|
.userId(provider.getUserId())
|
||||||
|
.build());
|
||||||
|
return user.getUser().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取身份信息所属的用户名.
|
||||||
|
* @param userId 身份信息所属的用户 Id.
|
||||||
|
* @return 返回用户名.
|
||||||
|
* @throws NullPointerException 当 userId 为 {@code null} 时抛出该异常.
|
||||||
|
* @throws NoSuchElementException 指定的 UserId 未找到对应用户名时抛出该异常.
|
||||||
|
*/
|
||||||
|
public String getIdentityName(String userId) {
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
if (!identityMap.containsKey(userId)) {
|
||||||
|
throw new NoSuchElementException(userId);
|
||||||
|
}
|
||||||
|
return identityNameMap.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 UserId 获取指定身份提供器.
|
||||||
|
* @param userId 用户 Id.
|
||||||
|
* @return 返回身份提供器.
|
||||||
|
* @throws NullPointerException 当 userId 为 {@code null} 时抛出该异常.
|
||||||
|
* @throws NoSuchElementException 指定的 UserId 未找到对应 Provider 时抛出该异常.
|
||||||
|
*/
|
||||||
|
public AuthenticationDetailsProvider getProviderByUserId(String userId) {
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
if (!identityMap.containsKey(userId)) {
|
||||||
|
throw new NoSuchElementException(userId);
|
||||||
|
}
|
||||||
|
return identityMap.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出身份信息.
|
||||||
|
* <p> 不包含私钥.
|
||||||
|
* @return 返回 Json 形式的身份信息数组.
|
||||||
|
*/
|
||||||
|
public JsonArray exportIdentityInfo() {
|
||||||
|
JsonArray identityInfoArray = new JsonArray(identityMap.size());
|
||||||
|
for (AuthenticationDetailsProvider provider : identityMap.values()) {
|
||||||
|
JsonObject identity = new JsonObject();
|
||||||
|
identity.addProperty("UserId", provider.getUserId());
|
||||||
|
identity.addProperty("TenantId", provider.getTenantId());
|
||||||
|
identity.addProperty("Fingerprint", provider.getFingerprint());
|
||||||
|
identityInfoArray.add(identity);
|
||||||
|
}
|
||||||
|
return identityInfoArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有身份提供器.
|
||||||
|
* @return 返回包含所有身份提供器的集合对象.
|
||||||
|
*/
|
||||||
|
public Set<AuthenticationDetailsProvider> getProviders() {
|
||||||
|
return identityMap.values().stream().collect(Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package net.lamgc.oracle.sentry.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class InputStreamWrapper extends InputStream {
|
||||||
|
|
||||||
|
private final InputStream source;
|
||||||
|
|
||||||
|
public InputStreamWrapper(InputStream source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return this.source.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
return this.source.read(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
return this.source.read(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readAllBytes() throws IOException {
|
||||||
|
return this.source.readAllBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readNBytes(int len) throws IOException {
|
||||||
|
return this.source.readNBytes(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int readNBytes(byte[] b, int off, int len) throws IOException {
|
||||||
|
return this.source.readNBytes(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
return this.source.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void skipNBytes(long n) throws IOException {
|
||||||
|
this.source.skipNBytes(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return this.source.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) {
|
||||||
|
this.source.mark(readlimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
this.source.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return this.source.markSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long transferTo(OutputStream out) throws IOException {
|
||||||
|
return this.source.transferTo(out);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.lamgc.oracle.sentry.common;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class OutputStreamWrapper extends OutputStream {
|
||||||
|
|
||||||
|
private final OutputStream target;
|
||||||
|
|
||||||
|
public OutputStreamWrapper(OutputStream target) {
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
target.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b) throws IOException {
|
||||||
|
this.target.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
this.target.write(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
this.target.flush();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute;
|
||||||
|
|
||||||
|
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
|
||||||
|
import com.oracle.bmc.core.ComputeClient;
|
||||||
|
import com.oracle.bmc.core.model.Image;
|
||||||
|
import com.oracle.bmc.core.model.Instance;
|
||||||
|
import com.oracle.bmc.core.requests.GetImageRequest;
|
||||||
|
import com.oracle.bmc.core.requests.GetInstanceRequest;
|
||||||
|
import com.oracle.bmc.core.requests.InstanceActionRequest;
|
||||||
|
import com.oracle.bmc.core.responses.GetImageResponse;
|
||||||
|
import com.oracle.bmc.core.responses.GetInstanceResponse;
|
||||||
|
import com.oracle.bmc.core.responses.InstanceActionResponse;
|
||||||
|
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ssh.InstanceSsh;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ssh.SshAuthInfo;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算实例.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final class ComputeInstance {
|
||||||
|
|
||||||
|
private final ComputeInstanceManager instanceManager;
|
||||||
|
|
||||||
|
private final String instanceId;
|
||||||
|
private final String userId;
|
||||||
|
private final String compartmentId;
|
||||||
|
private final String imageId;
|
||||||
|
private final AuthenticationDetailsProvider authProvider;
|
||||||
|
private final InstanceNetwork network;
|
||||||
|
|
||||||
|
private final ComputeClient computeClient;
|
||||||
|
|
||||||
|
public ComputeInstance(ComputeInstanceManager instanceManager, String instanceId, String userId,
|
||||||
|
String compartmentId, String imageId, AuthenticationDetailsProvider provider) {
|
||||||
|
this.instanceManager = instanceManager;
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
this.userId = userId;
|
||||||
|
this.compartmentId = compartmentId;
|
||||||
|
this.imageId = imageId;
|
||||||
|
this.authProvider = provider;
|
||||||
|
|
||||||
|
computeClient = new ComputeClient(provider);
|
||||||
|
this.network = new InstanceNetwork(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCompartmentId() {
|
||||||
|
return compartmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getImageId() {
|
||||||
|
return imageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取并返回实例镜像信息.
|
||||||
|
* <p> 可获取系统信息.
|
||||||
|
* <p> 如果实例被 dd, 则本信息不准确.
|
||||||
|
* @return 返回实例信息.
|
||||||
|
*/
|
||||||
|
public Image getImage() {
|
||||||
|
GetImageResponse image = computeClient.getImage(GetImageRequest.builder()
|
||||||
|
.imageId(imageId)
|
||||||
|
.build());
|
||||||
|
return image.getImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实例网络对象.
|
||||||
|
* <p> 可通过 InstanceNetwork 操作实例网络相关.
|
||||||
|
* @return 返回 InstanceNetwork.
|
||||||
|
*/
|
||||||
|
public InstanceNetwork network() {
|
||||||
|
return network;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstanceSsh ssh() {
|
||||||
|
Instance.LifecycleState instanceState = getInstanceState();
|
||||||
|
if (instanceState != Instance.LifecycleState.Running) {
|
||||||
|
throw new IllegalStateException("The state of the current instance cannot connect to SSH: " + instanceState);
|
||||||
|
}
|
||||||
|
return new InstanceSsh(this, getSshIdentity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instance.LifecycleState getInstanceState() {
|
||||||
|
GetInstanceResponse instance = computeClient.getInstance(GetInstanceRequest.builder()
|
||||||
|
.instanceId(instanceId)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return instance.getInstance().getLifecycleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对实例执行操作.
|
||||||
|
* @param action 操作类型.
|
||||||
|
* @return 如果成功, 返回实例最新状态.
|
||||||
|
*/
|
||||||
|
public Instance.LifecycleState execAction(InstanceAction action) {
|
||||||
|
InstanceActionResponse actionResponse = computeClient.instanceAction(InstanceActionRequest.builder()
|
||||||
|
.instanceId(instanceId)
|
||||||
|
.action(action.getActionValue())
|
||||||
|
.build());
|
||||||
|
return actionResponse.getInstance().getLifecycleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实例名称.
|
||||||
|
*/
|
||||||
|
public String getInstanceName() {
|
||||||
|
GetInstanceResponse instance = computeClient.getInstance(GetInstanceRequest.builder()
|
||||||
|
.instanceId(instanceId)
|
||||||
|
.build());
|
||||||
|
return instance.getInstance().getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得 OCI 的计算 API 客户端, 可通过该客户端执行更多的操作.
|
||||||
|
* @return 返回计算 API 客户端.
|
||||||
|
*/
|
||||||
|
public ComputeClient getComputeClient() {
|
||||||
|
return computeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationDetailsProvider getAuthProvider() {
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ComputeInstance that = (ComputeInstance) o;
|
||||||
|
return instanceId.equals(that.instanceId) && userId.equals(that.userId) && compartmentId.equals(that.compartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(instanceId, userId, compartmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 SSH 认证信息.
|
||||||
|
*/
|
||||||
|
private SshAuthInfo getSshIdentity() {
|
||||||
|
return instanceManager.getSshIdentityProvider()
|
||||||
|
.getAuthInfoByInstanceId(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute;
|
||||||
|
|
||||||
|
public enum InstanceAction {
|
||||||
|
/**
|
||||||
|
* 启动实例.
|
||||||
|
*/
|
||||||
|
START("start"),
|
||||||
|
/**
|
||||||
|
* 硬停止实例.
|
||||||
|
*/
|
||||||
|
STOP("stop"),
|
||||||
|
/**
|
||||||
|
* 硬重启实例.
|
||||||
|
*/
|
||||||
|
RESET("reset"),
|
||||||
|
/**
|
||||||
|
* 软重启实例, 操作系统将按照正常的重启过程进行.
|
||||||
|
*/
|
||||||
|
SOFT_RESET("softreset"),
|
||||||
|
/**
|
||||||
|
* 软停止实例, 操作系统将按照正常的关机过程进行.
|
||||||
|
*/
|
||||||
|
SOFT_STOP("softstop")
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String actionValue;
|
||||||
|
|
||||||
|
InstanceAction(String actionValue) {
|
||||||
|
this.actionValue = actionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActionValue() {
|
||||||
|
return actionValue;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute;
|
||||||
|
|
||||||
|
import com.oracle.bmc.core.VirtualNetworkClient;
|
||||||
|
import com.oracle.bmc.core.model.VnicAttachment;
|
||||||
|
import com.oracle.bmc.core.requests.GetVnicRequest;
|
||||||
|
import com.oracle.bmc.core.requests.ListVnicAttachmentsRequest;
|
||||||
|
import com.oracle.bmc.core.responses.GetVnicResponse;
|
||||||
|
import com.oracle.bmc.core.responses.ListVnicAttachmentsResponse;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class InstanceNetwork {
|
||||||
|
|
||||||
|
private final ComputeInstance instance;
|
||||||
|
private final VirtualNetworkClient vcnClient;
|
||||||
|
|
||||||
|
InstanceNetwork(ComputeInstance instance) {
|
||||||
|
this.instance = instance;
|
||||||
|
this.vcnClient = new VirtualNetworkClient(this.instance.getAuthProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实例的所有公共 IP.
|
||||||
|
* @return 返回所有公共 IP.
|
||||||
|
* @throws NoSuchElementException 当 InstanceId 所属实例未添加时抛出该异常.
|
||||||
|
* @throws NullPointerException 当 instanceId 为 {@code null} 时抛出该异常.
|
||||||
|
*/
|
||||||
|
public Set<String> getInstancePublicIp() {
|
||||||
|
Set<String> publicIpSet = new HashSet<>();
|
||||||
|
for (VnicAttachment vnicAttachment : listVnicAttachments()) {
|
||||||
|
GetVnicResponse vnic = vcnClient.getVnic(GetVnicRequest.builder()
|
||||||
|
.vnicId(vnicAttachment.getVnicId())
|
||||||
|
.build());
|
||||||
|
publicIpSet.add(vnic.getVnic().getPublicIp());
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicIpSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已连接的 Vnic 信息.
|
||||||
|
* @return 返回所有已连接的 Vnic.
|
||||||
|
*/
|
||||||
|
public List<VnicAttachment> listVnicAttachments() {
|
||||||
|
ListVnicAttachmentsResponse listVnicAttachments = instance.getComputeClient()
|
||||||
|
.listVnicAttachments(ListVnicAttachmentsRequest.builder()
|
||||||
|
.compartmentId(instance.getCompartmentId())
|
||||||
|
.instanceId(instance.getInstanceId())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
return listVnicAttachments.getItems().stream().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
|
||||||
|
import org.apache.sshd.client.SshClient;
|
||||||
|
import org.apache.sshd.client.future.AuthFuture;
|
||||||
|
import org.apache.sshd.client.future.ConnectFuture;
|
||||||
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
|
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||||
|
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class InstanceSsh implements AutoCloseable {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(InstanceSsh.class);
|
||||||
|
|
||||||
|
private final ComputeInstance instance;
|
||||||
|
private final SshAuthInfo authInfo;
|
||||||
|
private final SshClient sshClient;
|
||||||
|
|
||||||
|
public InstanceSsh(ComputeInstance instance, SshAuthInfo authInfo) {
|
||||||
|
this.instance = Objects.requireNonNull(instance);
|
||||||
|
this.authInfo = Objects.requireNonNull(authInfo);
|
||||||
|
|
||||||
|
sshClient = SshClient.setUpDefaultClient();
|
||||||
|
sshClient.setServerKeyVerifier(new OracleInstanceServerKeyVerifier(instance, authInfo));
|
||||||
|
if (authInfo instanceof PublicKeyAuthInfo info) {
|
||||||
|
sshClient.setKeyIdentityProvider(new FileKeyPairProvider(info.getPrivateKeyPath().toPath()));
|
||||||
|
if (!Strings.isNullOrEmpty(info.getKeyPassword())) {
|
||||||
|
sshClient.setFilePasswordProvider(FilePasswordProvider.of(info.getKeyPassword()));
|
||||||
|
}
|
||||||
|
} else if (authInfo instanceof PasswordAuthInfo info) {
|
||||||
|
sshClient.addPasswordIdentity(info.getPassword());
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported authentication type");
|
||||||
|
}
|
||||||
|
sshClient.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SshSession createSession() throws IOException {
|
||||||
|
Set<String> instancePublicIps = instance.network().getInstancePublicIp();
|
||||||
|
if (instancePublicIps.stream().findFirst().isEmpty()) {
|
||||||
|
throw new IllegalStateException("Instance has no public IP available.");
|
||||||
|
}
|
||||||
|
String connectUri = "ssh://" + authInfo.getUsername() + "@" + instancePublicIps.stream().findFirst().get() + ":22";
|
||||||
|
log.info("SSH 正在连接: {}", connectUri);
|
||||||
|
ConnectFuture connect = sshClient.connect(connectUri);
|
||||||
|
connect.verify();
|
||||||
|
if (!connect.isConnected()) {
|
||||||
|
if (connect.getException() != null) {
|
||||||
|
throw new IOException(connect.getException());
|
||||||
|
}
|
||||||
|
throw new IOException("A connection to the server could not be established for an unknown reason.");
|
||||||
|
}
|
||||||
|
ClientSession clientSession = connect.getClientSession();
|
||||||
|
AuthFuture auth = clientSession.auth();
|
||||||
|
auth.verify(20, TimeUnit.SECONDS);
|
||||||
|
if (auth.isSuccess()) {
|
||||||
|
return new SshSession(clientSession);
|
||||||
|
} else {
|
||||||
|
if (auth.isFailure()) {
|
||||||
|
clientSession.close();
|
||||||
|
throw new IOException("Authentication with server failed.", clientSession.auth().getException());
|
||||||
|
} else if (auth.isCanceled()) {
|
||||||
|
clientSession.close();
|
||||||
|
throw new IOException("Authentication cancelled.", clientSession.auth().getException());
|
||||||
|
}
|
||||||
|
throw new IOException("Authentication timeout.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
sshClient.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
|
||||||
|
import org.apache.sshd.client.keyverifier.RequiredServerKeyVerifier;
|
||||||
|
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
||||||
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
|
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class OracleInstanceServerKeyVerifier implements ServerKeyVerifier {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(OracleInstanceServerKeyVerifier.class);
|
||||||
|
|
||||||
|
private final ComputeInstance instance;
|
||||||
|
private final SshAuthInfo info;
|
||||||
|
|
||||||
|
public OracleInstanceServerKeyVerifier(ComputeInstance instance, SshAuthInfo info) {
|
||||||
|
this.instance = instance;
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
|
||||||
|
if (info.getServerKey() != null) {
|
||||||
|
return new RequiredServerKeyVerifier(info.getServerKey())
|
||||||
|
.verifyServerKey(clientSession, remoteAddress, serverKey);
|
||||||
|
} else {
|
||||||
|
log.warn("首次连接实例 SSH, 需要用户确认服务器公钥是否可信...");
|
||||||
|
boolean result = confirm(remoteAddress, serverKey);
|
||||||
|
if (result) {
|
||||||
|
log.info("用户已确认服务器密钥可信, 将该密钥列入该实例下的信任密钥.");
|
||||||
|
info.setServerKey(serverKey);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("用户已确认该密钥不可信, 拒绝本次连接.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean confirm(SocketAddress address, PublicKey key) {
|
||||||
|
String fingerPrint = KeyUtils.getFingerPrint(key);
|
||||||
|
log.warn("开始密钥认证流程... (InstanceId: {}, ServerAddress: {}, KeyFingerPrint: {})",
|
||||||
|
instance.getInstanceId(), address, fingerPrint);
|
||||||
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
log.info("""
|
||||||
|
本次连接 SSH 为首次连接, 为确保 SSH 安全性,请通过可信渠道获取服务器密钥指纹, 并与下列指纹比对:
|
||||||
|
实例 ID:{}
|
||||||
|
实例名称:{}
|
||||||
|
连接地址:{}
|
||||||
|
密钥指纹:
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
以上密钥指纹是否与服务器密钥指纹相同?如果指纹相同,对该密钥可信,请输入“Yes”,否则输入任意内容拒绝连接。
|
||||||
|
该密钥是否可信?(Yes/No):""",
|
||||||
|
instance.getInstanceId(),
|
||||||
|
instance.getInstanceName(),
|
||||||
|
address,
|
||||||
|
fingerPrint
|
||||||
|
);
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (scanner.hasNextLine()) {
|
||||||
|
String input = scanner.nextLine();
|
||||||
|
return "yes".trim().equalsIgnoreCase(input);
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class PasswordAuthInfo extends SshAuthInfo {
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthType getType() {
|
||||||
|
return AuthType.PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
public class PublicKeyAuthInfo extends SshAuthInfo{
|
||||||
|
|
||||||
|
private File privateKeyPath;
|
||||||
|
private String keyPassword;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthType getType() {
|
||||||
|
return AuthType.PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getPrivateKeyPath() {
|
||||||
|
return privateKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivateKeyPath(File privateKeyPath) {
|
||||||
|
this.privateKeyPath = privateKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyPassword() {
|
||||||
|
return keyPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyPassword(String keyPassword) {
|
||||||
|
this.keyPassword = keyPassword;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import com.google.common.reflect.TypeToken;
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||||
|
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
|
public final class SshAuthIdentityProvider {
|
||||||
|
|
||||||
|
private final static String DEFAULT_AUTH_KEY = "@default";
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(SshAuthIdentityProvider.class);
|
||||||
|
|
||||||
|
private final Map<String, SshAuthInfo> authInfoMap = new ConcurrentHashMap<>();
|
||||||
|
private final ComputeInstanceManager instanceManager;
|
||||||
|
private final File identityJsonFile;
|
||||||
|
private final Gson gson = new GsonBuilder()
|
||||||
|
.serializeNulls()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.registerTypeAdapter(SshAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
|
||||||
|
.registerTypeAdapter(PublicKeyAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
|
||||||
|
.registerTypeAdapter(PasswordAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduledExec = new ScheduledThreadPoolExecutor(1,
|
||||||
|
new ThreadFactoryBuilder()
|
||||||
|
.setNameFormat("Thread-SshInfoSave-%d")
|
||||||
|
.build());
|
||||||
|
private final AtomicBoolean needSave = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
|
||||||
|
public SshAuthIdentityProvider(ComputeInstanceManager instanceManager, File identityJson) {
|
||||||
|
this.instanceManager = instanceManager;
|
||||||
|
this.identityJsonFile = identityJson;
|
||||||
|
|
||||||
|
scheduledExec.scheduleAtFixedRate(() -> {
|
||||||
|
if (!needSave.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
needSave.set(false);
|
||||||
|
try {
|
||||||
|
SshAuthIdentityProvider.this.saveAuthInfo();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("本次 SSH 认证配置保存失败.", e);
|
||||||
|
}
|
||||||
|
}, 60, 10, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSshAuthIdentity(String instanceId, SshAuthInfo authInfo) {
|
||||||
|
authInfoMap.put(instanceId, authInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过实例 Id 获取相应的实例 Id.
|
||||||
|
* @param instanceId 实例 Id.
|
||||||
|
* @return 返回指定实例 Id 的 SSH 认证配置.
|
||||||
|
* @throws NoSuchElementException 当没有对应实例的配置时将抛出该异常.
|
||||||
|
*/
|
||||||
|
public SshAuthInfo getAuthInfoByInstanceId(String instanceId) {
|
||||||
|
if (!authInfoMap.containsKey(instanceId)) {
|
||||||
|
SshAuthInfo defaultAuthInfo = getDefaultAuthInfo();
|
||||||
|
if (defaultAuthInfo == null) {
|
||||||
|
throw new NoSuchElementException("The SSH authentication information to which the " +
|
||||||
|
"specified instance ID belongs cannot be found: " + instanceId);
|
||||||
|
}
|
||||||
|
return defaultAuthInfo;
|
||||||
|
}
|
||||||
|
return authInfoMap.get(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认配置.
|
||||||
|
* <p> 不建议使用, 因为服务器都不一样.
|
||||||
|
* @return 如果有, 返回配置, 没有默认配置则返回 {@code null}.
|
||||||
|
*/
|
||||||
|
public SshAuthInfo getDefaultAuthInfo() {
|
||||||
|
if (authInfoMap.containsKey(DEFAULT_AUTH_KEY)) {
|
||||||
|
return authInfoMap.get(DEFAULT_AUTH_KEY);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将所有认证配置保存到文件中.
|
||||||
|
* @throws IOException 如果保存时发生异常, 则抛出.
|
||||||
|
*/
|
||||||
|
private synchronized void saveAuthInfo() throws IOException {
|
||||||
|
log.info("正在保存 SSH 认证配置...");
|
||||||
|
String output = gson.toJson(authInfoMap);
|
||||||
|
Files.writeString(identityJsonFile.toPath(), output,
|
||||||
|
StandardCharsets.UTF_8, StandardOpenOption.CREATE);
|
||||||
|
log.info("已成功保存 SSH 认证配置.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知 Provider 需要保存配置.
|
||||||
|
*/
|
||||||
|
public void notifySaveInfo() {
|
||||||
|
needSave.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件加载所有认证配置.
|
||||||
|
* @throws IOException 如果读取文件时发生异常, 则抛出.
|
||||||
|
*/
|
||||||
|
public synchronized void loadAuthInfo() throws IOException {
|
||||||
|
if (!identityJsonFile.exists()) {
|
||||||
|
log.warn("SSH 认证配置文件不存在, 跳过加载.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, SshAuthInfo> map = gson.fromJson(new FileReader(identityJsonFile, StandardCharsets.UTF_8),
|
||||||
|
new TypeToken<Map<String, SshAuthInfo>>(){}.getType());
|
||||||
|
for (String id : map.keySet()) {
|
||||||
|
SshAuthInfo info = map.get(id);
|
||||||
|
info.setProvider(this);
|
||||||
|
addSshAuthIdentity(id, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> missingInstances = checkForMissingInstances();
|
||||||
|
if (missingInstances.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String missingInstanceId : missingInstances) {
|
||||||
|
builder.append(missingInstanceId).append('\n');
|
||||||
|
}
|
||||||
|
log.warn("以下实例不存在对应的 SSH 认证配置:\n{}", builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有不存在 SSH 配置的实例 Id.
|
||||||
|
*/
|
||||||
|
private Set<String> checkForMissingInstances() {
|
||||||
|
Set<String> instanceIdSet = instanceManager.getComputeInstances().stream()
|
||||||
|
.map(ComputeInstance::getInstanceId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
for (String id : authInfoMap.keySet()) {
|
||||||
|
instanceIdSet.remove(id);
|
||||||
|
}
|
||||||
|
return instanceIdSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ssh 认证信息.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public abstract class SshAuthInfo {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(SshAuthInfo.class);
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
/**
|
||||||
|
* 使用 Sha256 计算的密钥指纹.
|
||||||
|
*/
|
||||||
|
private PublicKey serverKey;
|
||||||
|
|
||||||
|
private SshAuthIdentityProvider provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认证类型.
|
||||||
|
* @return 返回认证类型.
|
||||||
|
*/
|
||||||
|
public abstract AuthType getType();
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey getServerKey() {
|
||||||
|
return serverKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServerKey(PublicKey serverKey) {
|
||||||
|
this.serverKey = serverKey;
|
||||||
|
if (this.provider != null) {
|
||||||
|
this.provider.notifySaveInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setProvider(SshAuthIdentityProvider provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuthType {
|
||||||
|
/**
|
||||||
|
* 密码认证.
|
||||||
|
*/
|
||||||
|
PASSWORD(PasswordAuthInfo.class),
|
||||||
|
/**
|
||||||
|
* 公钥认证
|
||||||
|
*/
|
||||||
|
PUBLIC_KEY(PublicKeyAuthInfo.class);
|
||||||
|
|
||||||
|
private final Class<? extends SshAuthInfo> targetClass;
|
||||||
|
|
||||||
|
AuthType(Class<? extends SshAuthInfo> targetClass) {
|
||||||
|
this.targetClass = targetClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends SshAuthInfo> getTargetClass() {
|
||||||
|
return targetClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gson.*;
|
||||||
|
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||||
|
import org.apache.sshd.common.config.keys.PublicKeyEntry;
|
||||||
|
import org.apache.sshd.common.config.keys.PublicKeyEntryDataResolver;
|
||||||
|
import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>, JsonDeserializer<SshAuthInfo> {
|
||||||
|
|
||||||
|
public final static SshAuthInfoSerializer INSTANCE = new SshAuthInfoSerializer();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SshAuthInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
if (!json.isJsonObject()) {
|
||||||
|
throw new JsonParseException("It should be a JsonObject");
|
||||||
|
}
|
||||||
|
JsonObject infoObject = json.getAsJsonObject();
|
||||||
|
String type = getFieldToStringOrFail(infoObject, "authType");
|
||||||
|
SshAuthInfo.AuthType authType = SshAuthInfo.AuthType.valueOf(type.toUpperCase());
|
||||||
|
SshAuthInfo info;
|
||||||
|
if (authType == SshAuthInfo.AuthType.PASSWORD) {
|
||||||
|
PasswordAuthInfo pswAuthInfo = new PasswordAuthInfo();
|
||||||
|
pswAuthInfo.setPassword(getFieldToStringOrFail(infoObject, "password"));
|
||||||
|
info = pswAuthInfo;
|
||||||
|
} else if (authType == SshAuthInfo.AuthType.PUBLIC_KEY) {
|
||||||
|
PublicKeyAuthInfo publicKeyInfo = new PublicKeyAuthInfo();
|
||||||
|
String privateKeyPath = getFieldToStringOrFail(infoObject, "privateKeyPath");
|
||||||
|
File privateKeyFile = new File(privateKeyPath);
|
||||||
|
publicKeyInfo.setPrivateKeyPath(privateKeyFile);
|
||||||
|
info = publicKeyInfo;
|
||||||
|
} else {
|
||||||
|
throw new JsonParseException("Unsupported authentication type: " + authType);
|
||||||
|
}
|
||||||
|
info.setUsername(getFieldToStringOrFail(infoObject, "username"));
|
||||||
|
try {
|
||||||
|
info.setServerKey(decodeSshPublicKey(
|
||||||
|
infoObject.has("serverKey") && infoObject.get("serverKey").isJsonPrimitive() ?
|
||||||
|
infoObject.get("serverKey").getAsString() :
|
||||||
|
null));
|
||||||
|
} catch (GeneralSecurityException | IOException e) {
|
||||||
|
throw new JsonParseException(e);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(SshAuthInfo src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("authType", src.getType().toString());
|
||||||
|
json.addProperty("username", src.getUsername());
|
||||||
|
try {
|
||||||
|
json.addProperty("serverKey", encodeSshPublicKey(src.getServerKey()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new JsonParseException(e);
|
||||||
|
}
|
||||||
|
if (src instanceof PasswordAuthInfo info) {
|
||||||
|
json.addProperty("password", info.getPassword());
|
||||||
|
} else if (src instanceof PublicKeyAuthInfo info) {
|
||||||
|
try {
|
||||||
|
json.addProperty("privateKeyPath", info.getPrivateKeyPath().getCanonicalPath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new JsonParseException(e);
|
||||||
|
}
|
||||||
|
json.addProperty("keyPassword", info.getKeyPassword());
|
||||||
|
} else {
|
||||||
|
throw new JsonParseException("Unsupported type");
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFieldToStringOrFail(JsonObject object, String field) {
|
||||||
|
if (!object.has(field)) {
|
||||||
|
throw new JsonParseException("Missing field: " + field);
|
||||||
|
}
|
||||||
|
return object.get(field).getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey decodeSshPublicKey(String publicKeyString) throws GeneralSecurityException, IOException {
|
||||||
|
if (Strings.isNullOrEmpty(publicKeyString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] strings = publicKeyString.split(" ", 3);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") PublicKeyEntryDecoder<PublicKey, ?> decoder =
|
||||||
|
(PublicKeyEntryDecoder<PublicKey, ?>) KeyUtils.getPublicKeyEntryDecoder(strings[0]);
|
||||||
|
return decoder.decodePublicKey(null, strings[0], Base64.getDecoder().decode(strings[1]), Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeSshPublicKey(PublicKey key) throws IOException {
|
||||||
|
if (key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
PublicKeyEntry.appendPublicKeyEntry(builder, key);
|
||||||
|
return builder.toString();
|
||||||
|
/*
|
||||||
|
// 以下代码改写自 KnownHosts 的那个认证器, 说实话翻一下官方代码还可以找到不错的东西.
|
||||||
|
@SuppressWarnings("unchecked") PublicKeyEntryDecoder<PublicKey, ?> decoder
|
||||||
|
= (PublicKeyEntryDecoder<PublicKey, ?>) KeyUtils.getPublicKeyEntryDecoder(key);
|
||||||
|
if (decoder == null) {
|
||||||
|
throw new JsonParseException("Cannot retrieve decoder for key=" + key.getAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream s = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
|
||||||
|
String keyType = decoder.encodePublicKey(s, key);
|
||||||
|
byte[] bytes = s.toByteArray();
|
||||||
|
PublicKeyEntryDataResolver encoder = PublicKeyEntry.resolveKeyDataEntryResolver(keyType);
|
||||||
|
return encoder.encodeEntryKeyData(bytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.common.InputStreamWrapper;
|
||||||
|
import net.lamgc.oracle.sentry.common.OutputStreamWrapper;
|
||||||
|
import org.apache.sshd.client.channel.ChannelExec;
|
||||||
|
import org.apache.sshd.client.channel.ClientChannelEvent;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
public final class SshExecChannel implements Closeable {
|
||||||
|
|
||||||
|
private final ChannelExec channelExec;
|
||||||
|
|
||||||
|
public SshExecChannel(ChannelExec channelExec) {
|
||||||
|
this.channelExec = channelExec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exec() throws IOException {
|
||||||
|
channelExec.open();
|
||||||
|
channelExec.waitFor(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.EXIT_SIGNAL), 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer exitCode() {
|
||||||
|
return channelExec.getExitStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIn(InputStream in) {
|
||||||
|
channelExec.setIn(new InputStreamWrapper(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOut(OutputStream out) {
|
||||||
|
channelExec.setOut(new OutputStreamWrapper(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErr(OutputStream err) {
|
||||||
|
channelExec.setErr(new OutputStreamWrapper(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
channelExec.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
|
import org.apache.sshd.sftp.client.SftpClient;
|
||||||
|
import org.apache.sshd.sftp.client.SftpClientFactory;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class SshSession implements Closeable {
|
||||||
|
|
||||||
|
private final ClientSession clientSession;
|
||||||
|
|
||||||
|
public SshSession(ClientSession clientSession) {
|
||||||
|
this.clientSession = clientSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SshExecChannel execCommand(String command) throws IOException {
|
||||||
|
return new SshExecChannel(clientSession.createExecChannel(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
clientSession.close();
|
||||||
|
}
|
||||||
|
}
|
17
src/main/java/net/lamgc/oracle/sentry/script/Script.java
Normal file
17
src/main/java/net/lamgc/oracle/sentry/script/Script.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.DelegatesTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public abstract class Script {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本信息.
|
||||||
|
* @return 返回脚本 ScriptInfo 对象.
|
||||||
|
*/
|
||||||
|
public abstract ScriptInfo getScriptInfo();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||||
|
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final record ScriptComponent(
|
||||||
|
ScriptHttpClient HTTP,
|
||||||
|
ComputeInstanceManager InstanceManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
60
src/main/java/net/lamgc/oracle/sentry/script/ScriptInfo.java
Normal file
60
src/main/java/net/lamgc/oracle/sentry/script/ScriptInfo.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脚本信息.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class ScriptInfo {
|
||||||
|
|
||||||
|
private String group;
|
||||||
|
private String artifact;
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
public String getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getArtifact() {
|
||||||
|
return artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getGroup() + ":" + getArtifact() + ":" + getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ScriptInfo that = (ScriptInfo) o;
|
||||||
|
return group.equals(that.group) && artifact.equals(that.artifact) && version.equals(that.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(group, artifact, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroup(String group) {
|
||||||
|
this.group = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArtifact(String artifact) {
|
||||||
|
this.artifact = artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(String version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脚本加载器.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public interface ScriptLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以加载.
|
||||||
|
* @param scriptFile 脚本文件.
|
||||||
|
* @return 如果可以加载, 返回 {@code true}.
|
||||||
|
*/
|
||||||
|
boolean canLoad(File scriptFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载脚本.
|
||||||
|
* @param context 脚本上下文.
|
||||||
|
* @param scriptFile 脚本文件.
|
||||||
|
* @return 返回脚本对象.
|
||||||
|
* @throws Exception 当 Loader 抛出异常时, 将视为脚本加载失败, 该脚本跳过加载.
|
||||||
|
*/
|
||||||
|
Script loadScript(ScriptComponent context, File scriptFile) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本信息.
|
||||||
|
* @param script 脚本对象.
|
||||||
|
* @return 返回脚本信息.
|
||||||
|
*/
|
||||||
|
ScriptInfo getScriptInfo(Script script);
|
||||||
|
|
||||||
|
}
|
119
src/main/java/net/lamgc/oracle/sentry/script/ScriptManager.java
Normal file
119
src/main/java/net/lamgc/oracle/sentry/script/ScriptManager.java
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脚本管理器.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final class ScriptManager {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(ScriptManager.class);
|
||||||
|
|
||||||
|
private final Set<ScriptLoader> loaders = new HashSet<>();
|
||||||
|
private final File scriptsLocation;
|
||||||
|
private final ScriptComponent context;
|
||||||
|
|
||||||
|
private final Map<ScriptInfo, Script> scripts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public ScriptManager(File scriptsLocation, ScriptComponent context) {
|
||||||
|
this.scriptsLocation = scriptsLocation;
|
||||||
|
this.context = context;
|
||||||
|
loadScriptLoaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void loadScriptLoaders() {
|
||||||
|
if (!loaders.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ServiceLoader<ScriptLoader> serviceLoader = ServiceLoader.load(ScriptLoader.class);
|
||||||
|
for (ScriptLoader scriptLoader : serviceLoader) {
|
||||||
|
loaders.add(scriptLoader);
|
||||||
|
}
|
||||||
|
log.info("存在 {} 个加载器可用.", loaders.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件中加载一个脚本.
|
||||||
|
* @param scriptFile 脚本文件.
|
||||||
|
* @return 如果加载成功, 返回 {@code true}, 加载失败或无加载器可用时返回 {@code false}
|
||||||
|
* @throws InvocationTargetException 当加载器加载脚本抛出异常时, 将通过该异常包装后抛出.
|
||||||
|
* @throws NullPointerException 当 scriptFile 为 {@code null} 时抛出.
|
||||||
|
*/
|
||||||
|
public boolean loadScript(File scriptFile) throws InvocationTargetException {
|
||||||
|
Objects.requireNonNull(scriptFile);
|
||||||
|
for (ScriptLoader loader : loaders) {
|
||||||
|
Script script;
|
||||||
|
try {
|
||||||
|
if (loader.canLoad(scriptFile)) {
|
||||||
|
script = loader.loadScript(context, scriptFile);
|
||||||
|
if (script == null) {
|
||||||
|
log.warn("加载器未能正确加载脚本, 已跳过该脚本.(ScriptName: {})", scriptFile.getName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("脚本加载时发生异常.(Loader: {}, Path: {})\n{}",
|
||||||
|
loader.getClass().getName(),
|
||||||
|
scriptFile.getAbsolutePath(),
|
||||||
|
Throwables.getStackTraceAsString(e));
|
||||||
|
throw new InvocationTargetException(e);
|
||||||
|
}
|
||||||
|
ScriptInfo scriptInfo = loader.getScriptInfo(script);
|
||||||
|
if (scriptInfo == null) {
|
||||||
|
log.warn("脚本加载成功, 但加载器没有返回脚本信息, 该脚本已放弃.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
scripts.put(scriptInfo, script);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从指定位置加载所有脚本.
|
||||||
|
*/
|
||||||
|
public void loadScripts() {
|
||||||
|
log.info("正在加载脚本...(Path: {})", scriptsLocation.getAbsolutePath());
|
||||||
|
File[] files = scriptsLocation.listFiles(File::isFile);
|
||||||
|
if (files == null) {
|
||||||
|
log.warn("脚本目录无法访问, 请检查程序是否有权限访问脚本目录.(Path: {})", scriptsLocation.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int loadCount = 0;
|
||||||
|
for (File scriptFile : files) {
|
||||||
|
try {
|
||||||
|
if (loadScript(scriptFile)) {
|
||||||
|
loadCount ++;
|
||||||
|
}
|
||||||
|
} catch (InvocationTargetException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("脚本已全部加载完成, 共成功加载了 {} 个脚本.", loadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过脚本信息返回脚本对象.
|
||||||
|
* @param scriptInfo 脚本信息.
|
||||||
|
* @return 返回脚本对象.
|
||||||
|
* @throws NoSuchElementException 当指定的 ScriptInfo 没有对应脚本对象时抛出该异常.
|
||||||
|
* @throws NullPointerException 当 scriptInfo 为 {@code null} 时抛出该异常.
|
||||||
|
*/
|
||||||
|
public Script getScriptByScriptInfo(ScriptInfo scriptInfo) {
|
||||||
|
Objects.requireNonNull(scriptInfo);
|
||||||
|
if (!scripts.containsKey(scriptInfo)) {
|
||||||
|
throw new NoSuchElementException(scriptInfo.toString());
|
||||||
|
}
|
||||||
|
return scripts.get(scriptInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||||
|
import net.lamgc.oracle.sentry.script.*;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.DelegatesTo;
|
||||||
|
import net.lamgc.oracle.sentry.script.groovy.trigger.*;
|
||||||
|
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||||
|
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groovy DSL 脚本的父类.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||||
|
public class GroovyDslDelegate extends Script {
|
||||||
|
|
||||||
|
private final GroovyScriptInfo scriptInfo = new GroovyScriptInfo();
|
||||||
|
private final ScriptHttpClient HTTP;
|
||||||
|
private final ComputeInstanceManager InstanceManager;
|
||||||
|
|
||||||
|
public GroovyDslDelegate(ScriptHttpClient httpClient, ComputeInstanceManager instanceManager) {
|
||||||
|
HTTP = httpClient;
|
||||||
|
InstanceManager = instanceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trigger(String triggerName, Closure<?> closure){
|
||||||
|
DefaultGroovyMethods.with(GroovyTriggerProvider.INSTANCE.getTriggerByName(triggerName), closure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脚本的基本信息.
|
||||||
|
* @param scriptInfoClosure 配置了脚本信息的闭包对象.
|
||||||
|
*/
|
||||||
|
private void info(@DelegatesTo(GroovyScriptInfo.class) Closure<GroovyScriptInfo> scriptInfoClosure) {
|
||||||
|
DefaultGroovyMethods.with(scriptInfo, scriptInfoClosure);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptInfo getScriptInfo() {
|
||||||
|
return scriptInfo;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptInfo;
|
||||||
|
|
||||||
|
public class GroovyScriptInfo extends ScriptInfo {
|
||||||
|
|
||||||
|
public void artifact(String artifact) {
|
||||||
|
super.setArtifact(artifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void group(String group) {
|
||||||
|
super.setGroup(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void version(String version) {
|
||||||
|
super.setVersion(version);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import groovy.lang.GroovyClassLoader;
|
||||||
|
import groovy.util.DelegatingScript;
|
||||||
|
import net.lamgc.oracle.sentry.script.Script;
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptComponent;
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptInfo;
|
||||||
|
import net.lamgc.oracle.sentry.script.ScriptLoader;
|
||||||
|
import org.codehaus.groovy.control.CompilerConfiguration;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("MapOrSetKeyShouldOverrideHashCodeEquals")
|
||||||
|
public class GroovyScriptLoader implements ScriptLoader {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(GroovyScriptLoader.class);
|
||||||
|
|
||||||
|
private final GroovyClassLoader scriptClassLoader;
|
||||||
|
|
||||||
|
private final Map<Script, ScriptInfo> scriptInfoMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public GroovyScriptLoader() {
|
||||||
|
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
|
||||||
|
compilerConfiguration.setScriptBaseClass(DelegatingScript.class.getName());
|
||||||
|
this.scriptClassLoader = new GroovyClassLoader(GroovyClassLoader.class.getClassLoader(), compilerConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canLoad(File scriptFile) {
|
||||||
|
return scriptFile.getName().endsWith(".groovy");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script loadScript(ScriptComponent context, File scriptFile) throws IOException {
|
||||||
|
Class<?> scriptClass = scriptClassLoader.parseClass(scriptFile);
|
||||||
|
if (!DelegatingScript.class.isAssignableFrom(scriptClass)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Constructor<? extends DelegatingScript> constructor =
|
||||||
|
scriptClass.asSubclass(DelegatingScript.class).getConstructor();
|
||||||
|
DelegatingScript newScriptObject = constructor.newInstance();
|
||||||
|
GroovyDslDelegate dslDelegate = new GroovyDslDelegate(context.HTTP(), context.InstanceManager());
|
||||||
|
newScriptObject.setDelegate(dslDelegate);
|
||||||
|
newScriptObject.run();
|
||||||
|
scriptInfoMap.put(dslDelegate, dslDelegate.getScriptInfo());
|
||||||
|
return dslDelegate;
|
||||||
|
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||||
|
log.error("加载脚本时发生异常.(ScriptPath: {})\n{}", scriptFile.getAbsolutePath(), Throwables.getStackTraceAsString(e));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptInfo getScriptInfo(Script script) {
|
||||||
|
return scriptInfoMap.get(script);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import net.lamgc.oracle.sentry.script.groovy.trigger.GroovyTrigger;
|
||||||
|
import net.lamgc.oracle.sentry.script.groovy.trigger.TriggerName;
|
||||||
|
import org.springframework.scheduling.Trigger;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class GroovyTriggerProvider {
|
||||||
|
|
||||||
|
private final Map<String, ServiceLoader.Provider<GroovyTrigger>> triggerProviderMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public final static GroovyTriggerProvider INSTANCE = new GroovyTriggerProvider();
|
||||||
|
|
||||||
|
private GroovyTriggerProvider() {
|
||||||
|
ServiceLoader<GroovyTrigger> loader = ServiceLoader.load(GroovyTrigger.class);
|
||||||
|
loader.stream().iterator().forEachRemaining(triggerProvider -> {
|
||||||
|
Class<? extends GroovyTrigger> triggerClass = triggerProvider.type();
|
||||||
|
if (!triggerClass.isAnnotationPresent(TriggerName.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TriggerName triggerName = triggerClass.getAnnotation(TriggerName.class);
|
||||||
|
if (!Strings.isNullOrEmpty(triggerName.value())) {
|
||||||
|
String name = triggerName.value().toLowerCase();
|
||||||
|
if (triggerProviderMap.containsKey(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
triggerProviderMap.put(name, triggerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroovyTrigger getTriggerByName(String triggerName) {
|
||||||
|
if (!triggerProviderMap.containsKey(triggerName.toLowerCase())) {
|
||||||
|
throw new NoSuchElementException("The specified trigger could not be found: " + triggerName);
|
||||||
|
}
|
||||||
|
return triggerProviderMap.get(triggerName).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy.trigger;
|
||||||
|
|
||||||
|
import groovy.lang.DelegatesTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public interface GroovyTrigger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动触发器.
|
||||||
|
* <p> 注意, 触发器执行 run 方法不可以阻塞方法返回.
|
||||||
|
* @param task 触发器需要执行的任务.
|
||||||
|
*/
|
||||||
|
void run(Runnable task);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy.trigger;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只执行一次的触发器, 执行后将不再执行该任务.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@TriggerName("once")
|
||||||
|
public class OnceTrigger implements GroovyTrigger {
|
||||||
|
|
||||||
|
private final static ExecutorService executor = Executors.newFixedThreadPool(4,
|
||||||
|
new ThreadFactoryBuilder()
|
||||||
|
.setNameFormat("GroovyOnceExec-%d")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(Runnable task) {
|
||||||
|
executor.execute(task);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy.trigger;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.scheduling.support.CronTrigger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@TriggerName("timer")
|
||||||
|
public class TimerTrigger implements GroovyTrigger {
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(TimerTrigger.class);
|
||||||
|
|
||||||
|
private CronTrigger trigger;
|
||||||
|
private final TaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设定定时时间.
|
||||||
|
* <p> 只允许在第一次执行时设置.
|
||||||
|
* @param expression Cron 时间表达式.
|
||||||
|
*/
|
||||||
|
public void time(String expression) {
|
||||||
|
if (trigger == null) {
|
||||||
|
trigger = new CronTrigger(expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(Runnable runnable) {
|
||||||
|
if (trigger == null) {
|
||||||
|
if (!log.isDebugEnabled()) {
|
||||||
|
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
|
||||||
|
} else {
|
||||||
|
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
|
||||||
|
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (runnable == null) {
|
||||||
|
if (!log.isDebugEnabled()) {
|
||||||
|
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
|
||||||
|
} else {
|
||||||
|
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
|
||||||
|
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.schedule(runnable, trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.groovy.trigger;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发器名称.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface TriggerName {
|
||||||
|
|
||||||
|
String value();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.tools.http;
|
||||||
|
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public class HttpAccess {
|
||||||
|
|
||||||
|
private final HttpClient client;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
HttpAccess(HttpClient client, String url) {
|
||||||
|
this.client = client;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpAccessResponse get() throws IOException {
|
||||||
|
HttpGet request = new HttpGet(url);
|
||||||
|
HttpResponse response = client.execute(request);
|
||||||
|
return new HttpAccessResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpAccessResponse post(String body) throws IOException {
|
||||||
|
HttpPost request = new HttpPost(url);
|
||||||
|
request.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
|
||||||
|
HttpResponse response = client.execute(request);
|
||||||
|
return new HttpAccessResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.tools.http;
|
||||||
|
|
||||||
|
import org.apache.http.Header;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.StatusLine;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Http 响应.
|
||||||
|
* @author LamGC
|
||||||
|
*/
|
||||||
|
public final class HttpAccessResponse {
|
||||||
|
|
||||||
|
private final StatusLine statusLine;
|
||||||
|
private final Locale locale;
|
||||||
|
private final Map<String, String> headers = new ConcurrentHashMap<>();
|
||||||
|
private final HttpEntity entity;
|
||||||
|
|
||||||
|
HttpAccessResponse(HttpResponse response) {
|
||||||
|
this.statusLine = response.getStatusLine();
|
||||||
|
this.locale = response.getLocale();
|
||||||
|
for (Header header : response.getAllHeaders()) {
|
||||||
|
headers.put(header.getName(), header.getValue());
|
||||||
|
}
|
||||||
|
this.entity = response.getEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusLine getStatusLine() {
|
||||||
|
return statusLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Locale getLocale() {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentToString() throws IOException {
|
||||||
|
return EntityUtils.toString(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpEntity getEntity() {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script.tools.http;
|
||||||
|
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
|
||||||
|
public class ScriptHttpClient {
|
||||||
|
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public ScriptHttpClient(HttpClient httpClient) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开一个连接.
|
||||||
|
* @param url 要访问的 Url.
|
||||||
|
*/
|
||||||
|
public HttpAccess create(String url) {
|
||||||
|
return new HttpAccess(httpClient, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
net.lamgc.oracle.sentry.script.groovy.GroovyScriptLoader
|
@ -0,0 +1,2 @@
|
|||||||
|
net.lamgc.oracle.sentry.script.groovy.trigger.OnceTrigger
|
||||||
|
net.lamgc.oracle.sentry.script.groovy.trigger.TimerTrigger
|
10
src/main/resources/application.yml
Normal file
10
src/main/resources/application.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
oracle:
|
||||||
|
identity:
|
||||||
|
# location: "C:\\Users\\80728\\.oci\\global\\"
|
||||||
|
script:
|
||||||
|
ssh:
|
||||||
|
identityPath: "./ssh.config.json"
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
75
src/main/resources/log4j2.xml
Normal file
75
src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2021 LamGC
|
||||||
|
~
|
||||||
|
~ ContentGrabbingJi is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU Affero General Public License as
|
||||||
|
~ published by the Free Software Foundation, either version 3 of the
|
||||||
|
~ License.
|
||||||
|
~
|
||||||
|
~ ContentGrabbingJi is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU Affero General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU Affero General Public License
|
||||||
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration status="WARN">
|
||||||
|
<!--
|
||||||
|
测试版跟发布版在日志配置文件上的区别仅仅只有'Loggers'的不同, 'properties'和'Appenders'是一致的.
|
||||||
|
-->
|
||||||
|
<properties>
|
||||||
|
<property name="logStorePath">./logs</property>
|
||||||
|
<property name="charset">UTF-8</property>
|
||||||
|
<property name="standard_pattern">[%-d{HH:mm:ss.SSS} %5level][%logger.%method():%-3L][%thread]: %msg%n</property>
|
||||||
|
<property name="mirai_pattern">[%-d{HH:mm:ss.SSS} %5level][%logger]: %msg%n</property>
|
||||||
|
<property name="logsDir">${sys:cgj.logsPath:-logs}</property>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<Appenders>
|
||||||
|
<Console name="STANDARD_STDOUT" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout charset="${charset}">
|
||||||
|
<MarkerPatternSelector defaultPattern="${standard_pattern}">
|
||||||
|
<PatternMatch key="mirai" pattern="${mirai_pattern}" />
|
||||||
|
</MarkerPatternSelector>
|
||||||
|
</PatternLayout>
|
||||||
|
<Filters>
|
||||||
|
<LevelRangeFilter minLevel="INFO" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||||
|
</Filters>
|
||||||
|
</Console>
|
||||||
|
<Console name="STANDARD_STDERR" target="SYSTEM_ERR">
|
||||||
|
<PatternLayout charset="${charset}">
|
||||||
|
<MarkerPatternSelector defaultPattern="${standard_pattern}">
|
||||||
|
<PatternMatch key="mirai" pattern="${mirai_pattern}" />
|
||||||
|
</MarkerPatternSelector>
|
||||||
|
</PatternLayout>
|
||||||
|
<Filters>
|
||||||
|
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||||
|
</Filters>
|
||||||
|
</Console>
|
||||||
|
|
||||||
|
<RollingFile name="rollingFile" fileName="${logsDir}/latest.log" filePattern="${logsDir}/running.%-d{yyyy-MM-dd_HH-mm-ss}.log.gz">
|
||||||
|
<Filters>
|
||||||
|
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||||
|
</Filters>
|
||||||
|
<PatternLayout charset="${charset}">
|
||||||
|
<MarkerPatternSelector defaultPattern="${standard_pattern}">
|
||||||
|
<PatternMatch key="mirai" pattern="${mirai_pattern}" />
|
||||||
|
</MarkerPatternSelector>
|
||||||
|
</PatternLayout>
|
||||||
|
<Policies>
|
||||||
|
<OnStartupTriggeringPolicy />
|
||||||
|
</Policies>
|
||||||
|
</RollingFile>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Root level="DEBUG">
|
||||||
|
<AppenderRef ref="STANDARD_STDOUT"/>
|
||||||
|
<AppenderRef ref="STANDARD_STDERR"/>
|
||||||
|
<AppenderRef ref="rollingFile"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</configuration>
|
@ -0,0 +1,50 @@
|
|||||||
|
package net.lamgc.oracle.sentry.oci.compute.ssh;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class SshAuthInfoSerializerTest {
|
||||||
|
|
||||||
|
private final static Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(SshAuthInfo.class, new SshAuthInfoSerializer())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
private JsonObject getPasswordAuthObject() {
|
||||||
|
return gson.fromJson("""
|
||||||
|
{
|
||||||
|
"username": "opc",
|
||||||
|
"authType": "password",
|
||||||
|
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
|
||||||
|
"password": "123456"
|
||||||
|
}
|
||||||
|
""", JsonObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject getPublicKeyAuthObject() {
|
||||||
|
return gson.fromJson("""
|
||||||
|
{
|
||||||
|
"username": "opc",
|
||||||
|
"authType": "Public_Key",
|
||||||
|
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
|
||||||
|
"privateKeyPath": "",
|
||||||
|
"keyPassword": ""
|
||||||
|
}
|
||||||
|
""", JsonObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deserializeTest() {
|
||||||
|
SshAuthInfo info = gson.fromJson(getPasswordAuthObject(), SshAuthInfo.class);
|
||||||
|
|
||||||
|
assertTrue(info instanceof PasswordAuthInfo);
|
||||||
|
assertEquals("opc", info.getUsername());
|
||||||
|
assertEquals("123456", ((PasswordAuthInfo) info).getPassword());
|
||||||
|
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.lamgc.oracle.sentry.script;
|
||||||
|
|
||||||
|
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||||
|
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
class ScriptManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loadScriptTest() {
|
||||||
|
ScriptManager manager = new ScriptManager(new File("./run/scripts"),
|
||||||
|
new ScriptComponent(new ScriptHttpClient(HttpClientBuilder.create().build()),
|
||||||
|
new ComputeInstanceManager()));
|
||||||
|
|
||||||
|
manager.loadScripts();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user