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