[Initial] Initial Commit;

This commit is contained in:
LamGC 2021-08-13 00:29:19 +08:00
commit 3dda31efb7
Signed by: LamGC
GPG Key ID: 6C5AE2A913941E1D
46 changed files with 2629 additions and 0 deletions

33
.gitignore vendored Normal file
View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
rootProject.name = 'oracle-sentry'

View 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;
}
}

View File

@ -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());
}
}

View 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());
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}*/
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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();
}

View File

@ -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
) {
}

View 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;
}
}

View File

@ -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);
}

View 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);
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

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

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
net.lamgc.oracle.sentry.script.groovy.GroovyScriptLoader

View File

@ -0,0 +1,2 @@
net.lamgc.oracle.sentry.script.groovy.trigger.OnceTrigger
net.lamgc.oracle.sentry.script.groovy.trigger.TimerTrigger

View File

@ -0,0 +1,10 @@
oracle:
identity:
# location: "C:\\Users\\80728\\.oci\\global\\"
script:
ssh:
identityPath: "./ssh.config.json"
logging:
level:
root: INFO

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

View File

@ -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()));
}
}

View File

@ -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();
}
}