diff --git a/.gitignore b/.gitignore index da7a791..a9cac65 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.project /.settings/ /logs/ +/data/ diff --git a/conf/springboot.yml b/conf/springboot.yml index 6fd22eb..b9a5160 100644 --- a/conf/springboot.yml +++ b/conf/springboot.yml @@ -7,3 +7,6 @@ ssh-server: regex-mapping: location: "conf/regex-mapping.properties" +spring: + datasource: + url: "jdbc:h2:file:./data/remote-ip-info-db" \ No newline at end of file diff --git a/pom.xml b/pom.xml index d4b8507..d8b115c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.example.sshd echo-sshd-server - 1.3.0 + 1.4.0 ECHO SSH SERVER Learning Apache Mina SSHD library @@ -13,10 +13,11 @@ 3.3.2 + 17 2.0.25 0.14.0 - 1.78.1 2.13.0 + 1.4.0 @@ -33,6 +34,20 @@ org.springframework.boot spring-boot-starter-log4j2 + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.h2database + h2 + runtime + + + org.apache.commons + commons-exec + ${commons-exec.version} + commons-codec commons-codec @@ -47,16 +62,6 @@ sshd-core ${sshd.version} - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} - commons-io commons-io @@ -77,8 +82,8 @@ org.apache.maven.plugins maven-compiler-plugin - 17 - 17 + ${java.version} + ${java.version} diff --git a/src/main/java/com/example/sshd/core/EchoSessionListener.java b/src/main/java/com/example/sshd/core/EchoSessionListener.java index 28f5788..f842e9a 100644 --- a/src/main/java/com/example/sshd/core/EchoSessionListener.java +++ b/src/main/java/com/example/sshd/core/EchoSessionListener.java @@ -1,6 +1,7 @@ package com.example.sshd.core; import java.net.InetSocketAddress; +import java.util.List; import java.util.Map; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; @@ -15,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import com.example.sshd.service.JdbcService; + @Component public class EchoSessionListener implements SessionListener { @@ -30,6 +33,9 @@ public class EchoSessionListener implements SessionListener { @Autowired CloseableHttpAsyncClient asyncClient; + @Autowired + JdbcService jdbcService; + @Value("${ssh-server.ip-info-api.url:http://ip-api.com/json/%s}") private String ipInfoApiUrl; @@ -48,6 +54,12 @@ public class EchoSessionListener implements SessionListener { } logger.info("new session: {} -> {}", remoteIpAddress, session); remoteSessionMapping.put(remoteIpAddress, session); + if (!ipInfoMapping.containsKey(remoteIpAddress)) { + List> ipInfoList = jdbcService.getRemoteIpInfo(remoteIpAddress); + if (!ipInfoList.isEmpty()) { + ipInfoMapping.put(remoteIpAddress, (String) ipInfoList.get(0).get("remote_ip_info")); + } + } } } @@ -64,15 +76,19 @@ public class EchoSessionListener implements SessionListener { @Override public void completed(SimpleHttpResponse result) { - logger.info("[{}] asyncClient.execute completed, result: {}, content-type: {}, body: {}", + logger.info( + "[{}] asyncClient.execute completed, result: {}, content-type: {}, body: {}", remoteIpAddress, result, result.getContentType(), result.getBodyText()); ipInfoMapping.put(remoteIpAddress, result.getBodyText()); - ipInfoLogger.info("[{}] {}", remoteIpAddress, ipInfoMapping.get(remoteIpAddress)); + int inserted = jdbcService.insertRemoteIpInfo(remoteIpAddress, result.getBodyText()); + ipInfoLogger.info("[{}] {}, inserted = {}", remoteIpAddress, + ipInfoMapping.get(remoteIpAddress), inserted); } @Override public void failed(Exception exception) { - logger.info("[{}] asyncClient.execute failed, exception: {}", remoteIpAddress, exception); + logger.info("[{}] asyncClient.execute failed, exception: {}", remoteIpAddress, + exception); } @Override diff --git a/src/main/java/com/example/sshd/core/EchoShell.java b/src/main/java/com/example/sshd/core/EchoShell.java index d0771a3..a1e9112 100644 --- a/src/main/java/com/example/sshd/core/EchoShell.java +++ b/src/main/java/com/example/sshd/core/EchoShell.java @@ -21,7 +21,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import com.example.sshd.util.ReplyUtil; +import com.example.sshd.service.ReplyService; @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @@ -30,7 +30,7 @@ public class EchoShell implements Command, Runnable, SessionAware { private static final Logger logger = LoggerFactory.getLogger(EchoShell.class); @Autowired - ReplyUtil replyUtil; + ReplyService replyUtil; @Autowired Properties hashReplies; diff --git a/src/main/java/com/example/sshd/service/JdbcService.java b/src/main/java/com/example/sshd/service/JdbcService.java new file mode 100644 index 0000000..24a611c --- /dev/null +++ b/src/main/java/com/example/sshd/service/JdbcService.java @@ -0,0 +1,49 @@ +package com.example.sshd.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; + +@Service +public class JdbcService { + + private static final String createRemoteIpLookupTableSql = "CREATE TABLE IF NOT EXISTS public.remote_ip_lookup (id BIGINT not null, " + + "remote_ip_address CHARACTER VARYING not null, remote_ip_info CHARACTER VARYING not null, PRIMARY KEY (id));"; + private static final String createRemoteIpLookupIndexSql = "CREATE INDEX IF NOT EXISTS public.remote_ip_lookup_idx ON " + + "public.remote_ip_lookup (remote_ip_address);"; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @PostConstruct + private void init() { + jdbcTemplate.execute(createRemoteIpLookupTableSql); + jdbcTemplate.execute(createRemoteIpLookupIndexSql); + } + + public List> getRemoteIpInfo(String remoteIp) { + return jdbcTemplate.query( + "SELECT id, remote_ip_address, remote_ip_info from public.remote_ip_lookup WHERE remote_ip_address = ? ", + new RowMapper>() { + @Override + public Map mapRow(ResultSet rs, int rowNum) throws SQLException { + return Map.of("id", rs.getLong(1), "remote_ip_address", rs.getString(2), "remote_ip_info", + rs.getString(3)); + } + }, remoteIp); + } + + public int insertRemoteIpInfo(String remoteIpAddress, String remoteIpInfo) { + return jdbcTemplate.update( + "INSERT INTO public.remote_ip_lookup (id, remote_ip_address, remote_ip_info) VALUES (?, ?, ?)", + System.currentTimeMillis(), remoteIpAddress, remoteIpInfo); + } +} diff --git a/src/main/java/com/example/sshd/util/ReplyUtil.java b/src/main/java/com/example/sshd/service/ReplyService.java similarity index 70% rename from src/main/java/com/example/sshd/util/ReplyUtil.java rename to src/main/java/com/example/sshd/service/ReplyService.java index 1b8020f..6102ef3 100644 --- a/src/main/java/com/example/sshd/util/ReplyUtil.java +++ b/src/main/java/com/example/sshd/service/ReplyService.java @@ -1,25 +1,30 @@ -package com.example.sshd.util; +package com.example.sshd.service; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Map; import java.util.Optional; import java.util.Properties; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.sshd.server.session.ServerSession; -import org.bouncycastle.util.encoders.Base64; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component -public class ReplyUtil { +public class ReplyService { - private static final Logger logger = LoggerFactory.getLogger(ReplyUtil.class); + private static final Logger logger = LoggerFactory.getLogger(ReplyService.class); private static final Logger notFoundLogger = LoggerFactory.getLogger("not_found"); @Autowired @@ -56,17 +61,30 @@ public class ReplyUtil { } else if (hashReplies.containsKey(String.format("base64(%s)", cmdHash))) { logger.info("[{}] Known base64-hash detected: {}", cmdHash, command.trim()); String reply = hashReplies.getProperty(String.format("base64(%s)", cmdHash)); - reply = new String(Base64.decode(reply)); + reply = new String(Base64.decodeBase64(reply)); out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); } else { Optional> o = regexMapping.entrySet().stream() .filter(e -> command.trim().matches(((String) e.getKey()))) .map(e -> Pair.of((String) e.getKey(), (String) e.getValue())).findAny(); if (o.isPresent()) { - logger.info("[{}] Known pattern detected: {} ({})", cmdHash, command.trim(), o.get()); String reply = hashReplies.getProperty(o.get().getRight(), "").replace("\\r", "\r").replace("\\n", "\n") .replace("\\t", "\t"); - out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); + if (reply.isEmpty()) { + logger.info("[{}] Execute cmd for real: {} ({})", cmdHash, command.trim(), o.get()); + CommandLine cmdLine = CommandLine.parse(command.trim()); + DefaultExecutor executor = DefaultExecutor.builder().get(); + ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); + PumpStreamHandler streamHandler = new PumpStreamHandler(tempOut); + executor.setStreamHandler(streamHandler); + int exitValue = executor.execute(cmdLine); + logger.info("[{}] Result: {} ({})", cmdHash, command.trim(), exitValue); + reply = new String(tempOut.toByteArray()).replace("\n", "\r\n"); + out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); + } else { + logger.info("[{}] Known pattern detected: {} ({})", cmdHash, command.trim(), o.get()); + out.write(String.format("\r\n%s\r\n%s", reply, prompt).getBytes()); + } } else { logger.info("[{}] Command not found: {}", cmdHash, command.trim()); notFoundLogger.info("[{}] Command not found: {}", cmdHash, command.trim());