diff --git a/pom.xml b/pom.xml
index e1e09c3..18bcc1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.example.sshd
echo-sshd-server
- 2.0.0
+ 2.1.0
ECHO SSH SERVER
Learning Apache Mina SSHD library
@@ -44,6 +44,10 @@
+
+ com.github.ben-manes.caffeine
+ caffeine
+
com.fasterxml.jackson.core
jackson-core
diff --git a/src/main/java/com/example/sshd/config/AppConfig.java b/src/main/java/com/example/sshd/config/AppConfig.java
index e13ac8d..3adec8e 100644
--- a/src/main/java/com/example/sshd/config/AppConfig.java
+++ b/src/main/java/com/example/sshd/config/AppConfig.java
@@ -1,6 +1,7 @@
package com.example.sshd.config;
import java.io.IOException;
+import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -15,10 +16,13 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
+import org.xmpp.packet.Message;
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
@Configuration
public class AppConfig {
@@ -32,6 +36,11 @@ public class AppConfig {
return new ObjectMapper().readValue(new java.io.File(xmppComponentConfigJson), XmppComponentConfig.class);
}
+ @Bean("userAdminCache")
+ public Cache userAdminCache() {
+ return Caffeine.newBuilder().expireAfterWrite(Duration.ofMinutes(1)).build();
+ }
+
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Map remoteSessionMapping() {
diff --git a/src/main/java/com/example/sshd/service/EchoComponent.java b/src/main/java/com/example/sshd/service/EchoComponent.java
index fdeb1dc..65518ba 100644
--- a/src/main/java/com/example/sshd/service/EchoComponent.java
+++ b/src/main/java/com/example/sshd/service/EchoComponent.java
@@ -1,16 +1,22 @@
package com.example.sshd.service;
import org.apache.commons.lang3.StringUtils;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
import org.jivesoftware.whack.ExternalComponentManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.xmpp.component.AbstractComponent;
import org.xmpp.component.ComponentException;
+import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
+import org.xmpp.packet.IQ.Type;
import com.example.sshd.config.XmppComponentConfig;
+import com.github.benmanes.caffeine.cache.Cache;
import jakarta.annotation.PostConstruct;
@@ -18,10 +24,17 @@ import jakarta.annotation.PostConstruct;
public class EchoComponent extends AbstractComponent {
private static final Logger logger = LoggerFactory.getLogger(EchoComponent.class);
+ public static final String CONST_OPERATION_ADD_USER = "adduser";
+ public static final String CONST_OPERATION_CHANGE_USER_PASSWORD = "chgpasswd";
+ public static final String CONST_OPERATION_DELETE_USER = "deluser";
@Autowired
XmppComponentConfig xmppComponentConfig;
+ @Autowired
+ @Qualifier("userAdminCache")
+ private volatile Cache userAdminCache;
+
ExternalComponentManager externalComponentManager = null;
@PostConstruct
@@ -35,6 +48,13 @@ public class EchoComponent extends AbstractComponent {
xmppComponentConfig.getSecretKey());
externalComponentManager.addComponent(xmppComponentConfig.getSubdomainPrefix(), this,
xmppComponentConfig.getPort());
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ externalComponentManager.removeComponent(xmppComponentConfig.getSubdomainPrefix());
+ } catch (ComponentException e) {
+ e.printStackTrace();
+ }
+ }));
}
@Override
@@ -47,20 +67,222 @@ public class EchoComponent extends AbstractComponent {
return this.getClass().getName();
}
+ private void doEcho(final Message inMsg, String body) {
+ try {
+ Message outMsg = new Message();
+ outMsg.setType(inMsg.getType());
+ outMsg.setFrom(inMsg.getTo());
+ if (StringUtils.endsWith(inMsg.getSubject(), "@" + xmppComponentConfig.getDomain())) {
+ outMsg.setTo(inMsg.getSubject());
+ } else {
+ outMsg.setTo(inMsg.getFrom());
+ }
+ outMsg.setSubject(inMsg.getSubject());
+ outMsg.setBody(body == null ? inMsg.getBody() : body);
+ externalComponentManager.sendPacket(this, outMsg);
+ logger.info("[doEcho] -- SENT -- {}", outMsg);
+ } catch (Exception err) {
+ logger.error("[doEcho] ", err);
+ }
+ }
+
protected void handleMessage(final Message inMsg) {
- logger.info("-- RECEIVED -- {}", inMsg);
- Message outMsg = new Message();
- outMsg.setType(inMsg.getType());
- outMsg.setFrom(inMsg.getTo());
- if (StringUtils.endsWith(inMsg.getSubject(), "@" + xmppComponentConfig.getDomain())) {
- outMsg.setTo(inMsg.getSubject());
- } else {
- outMsg.setTo(inMsg.getFrom());
+ logger.info("[handleMessage] -- RECEIVED -- {}", inMsg);
+ try {
+ if (StringUtils.isNotBlank(inMsg.getBody())) {
+ String[] commandParts = StringUtils.split(inMsg.getBody(), ' ');
+ switch (commandParts[0]) {
+ case CONST_OPERATION_ADD_USER:
+ if (commandParts.length == 3)
+ requestAddUserForm(inMsg);
+ else
+ doEcho(inMsg, "adduser ");
+ break;
+ case CONST_OPERATION_DELETE_USER:
+ if (commandParts.length == 2)
+ requestDeleteUserForm(inMsg);
+ else
+ doEcho(inMsg, "deluser ");
+ break;
+ case CONST_OPERATION_CHANGE_USER_PASSWORD:
+ if (commandParts.length == 3)
+ requestChangeUserPassword(inMsg);
+ else
+ doEcho(inMsg, "chgpasswd ");
+ break;
+ default:
+ doEcho(inMsg, null);
+ break;
+ }
+ }
+ } catch (Exception err) {
+ logger.error("[handleMessage] ", err);
+ }
+ }
+
+ @Override
+ protected void handleIQResult(IQ iq) {
+ try {
+ logger.debug("[handleIQResult] {} has received iq-result: {}", getName(), iq);
+ if (iq.getChildElement() != null) {
+ logger.debug("[{}] {}'s child-namespace: {}", iq.getID(), getName(),
+ iq.getChildElement().getNamespace());
+ logger.debug("[{}] {}'s child-name: {}", iq.getID(), getName(), iq.getChildElement().getName());
+ if (iq.getChildElement().getNamespace().equals(Namespace.get("http://jabber.org/protocol/commands"))
+ && iq.getChildElement().getName().equals("command")) {
+ Message inMsg = userAdminCache.getIfPresent(iq.getID());
+ handleCommands(iq.getID(), inMsg, iq.getChildElement());
+ }
+ }
+ } catch (Exception err) {
+ logger.error("[handleIQResult] ", err);
+ }
+ }
+
+ protected void handleCommands(String id, Message inMsg, Element command) throws InterruptedException {
+ String status = command.attributeValue("status");
+ String node = command.attributeValue("node");
+ String sessionid = command.attributeValue("sessionid");
+ logger.debug("[{}] sessionid: {}, status: {}, node: {}", id, sessionid, status, node);
+ if (status.equals("executing")) {
+ if (node.equals("http://jabber.org/protocol/admin#add-user")) {
+ sendAddUserForm(sessionid, inMsg);
+ } else if (node.equals("http://jabber.org/protocol/admin#delete-user")) {
+ sendDeleteUserForm(sessionid, inMsg);
+ } else if (node.equals("http://jabber.org/protocol/admin#change-user-password")) {
+ sendChangeUserPasswordForm(sessionid, inMsg);
+ }
+ } else if (status.equals("completed")) {
+ doEcho(inMsg, "OK");
+ }
+ }
+
+ public void requestAddUserForm(Message inMsg) {
+ try {
+ IQ addUserIq = new IQ(Type.set);
+ addUserIq.setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ addUserIq.setTo(xmppComponentConfig.getDomain());
+ Element child = addUserIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("action", "execute");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#add-user");
+ userAdminCache.put(addUserIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, addUserIq);
+ logger.info("[requestAddUserForm] -- SENT -- {}", addUserIq);
+ } catch (Exception err) {
+ logger.error("[requestAddUserForm] ", err);
+ }
+ }
+
+ private void createFormTypeElement(Element x, String var, String type, String value) {
+ Element formType = x.addElement("field");
+ formType.addAttribute("var", var);
+ if (type != null) {
+ formType.addAttribute("type", type);
+ }
+ formType.addElement("value").setText(value);
+ }
+
+ public void sendAddUserForm(String sessionId, Message inMsg) {
+ try {
+ String[] commandParts = StringUtils.split(inMsg.getBody(), ' ');
+ IQ addUserIq = new IQ(Type.set);
+ addUserIq.setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ addUserIq.setTo(xmppComponentConfig.getDomain());
+ Element child = addUserIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#add-user");
+ child.addAttribute("sessionid", sessionId);
+ Element x = child.addElement("x", "jabber:x:data");
+ x.addAttribute("type", "submit");
+ createFormTypeElement(x, "FORM_TYPE", "hidden", "http://jabber.org/protocol/admin");
+ createFormTypeElement(x, "accountjid", "jid-single",
+ commandParts[1] + "@" + xmppComponentConfig.getDomain());
+ createFormTypeElement(x, "password", "text-private", commandParts[2]);
+ createFormTypeElement(x, "password-verify", "text-private", commandParts[2]);
+ userAdminCache.put(addUserIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, addUserIq);
+ logger.info("[sendAddUserForm] -- SENT -- {}", addUserIq);
+ } catch (Exception err) {
+ logger.error("[sendAddUserForm] ", err);
+ }
+ }
+
+ public void requestDeleteUserForm(Message inMsg) {
+ try {
+ IQ deleteUserIq = new IQ(Type.set);
+ deleteUserIq.setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ deleteUserIq.setTo(xmppComponentConfig.getDomain());
+ Element child = deleteUserIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("action", "execute");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#delete-user");
+ userAdminCache.put(deleteUserIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, deleteUserIq);
+ logger.info("[requestDeleteUserForm] -- SENT -- {}", deleteUserIq);
+ } catch (Exception err) {
+ logger.error("[requestDeleteUserForm] ", err);
+ }
+ }
+
+ public void sendDeleteUserForm(String sessionId, Message inMsg) {
+ try {
+ String[] commandParts = StringUtils.split(inMsg.getBody(), ' ');
+ IQ deleteUserIq = new IQ(Type.set);
+ deleteUserIq.setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ deleteUserIq.setTo(xmppComponentConfig.getDomain());
+ Element child = deleteUserIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#delete-user");
+ child.addAttribute("sessionid", sessionId);
+ Element x = child.addElement("x", "jabber:x:data");
+ x.addAttribute("type", "submit");
+ createFormTypeElement(x, "FORM_TYPE", "hidden", "http://jabber.org/protocol/admin");
+ createFormTypeElement(x, "accountjids", "jid-single",
+ commandParts[1] + "@" + xmppComponentConfig.getDomain());
+ userAdminCache.put(deleteUserIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, deleteUserIq);
+ logger.info("[sendDeleteUserForm] -- SENT -- {}", deleteUserIq);
+ } catch (Exception err) {
+ logger.error("[sendDeleteUserForm] ", err);
+ }
+ }
+
+ public void requestChangeUserPassword(Message inMsg) {
+ try {
+ IQ changeUserPasswordIq = new IQ(Type.set);
+ changeUserPasswordIq
+ .setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ changeUserPasswordIq.setTo(xmppComponentConfig.getDomain());
+ Element child = changeUserPasswordIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("action", "execute");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#change-user-password");
+ userAdminCache.put(changeUserPasswordIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, changeUserPasswordIq);
+ logger.info("[requestChangeUserPassword] -- SENT -- {}", changeUserPasswordIq);
+ } catch (Exception err) {
+ logger.error("[requestChangeUserPassword] ", err);
+ }
+ }
+
+ public void sendChangeUserPasswordForm(String sessionId, Message inMsg) {
+ try {
+ String[] commandParts = StringUtils.split(inMsg.getBody(), ' ');
+ IQ changeUserPasswordIq = new IQ(Type.set);
+ changeUserPasswordIq
+ .setFrom(xmppComponentConfig.getSubdomainPrefix() + "." + xmppComponentConfig.getDomain());
+ changeUserPasswordIq.setTo(xmppComponentConfig.getDomain());
+ Element child = changeUserPasswordIq.setChildElement("command", "http://jabber.org/protocol/commands");
+ child.addAttribute("node", "http://jabber.org/protocol/admin#change-user-password");
+ child.addAttribute("sessionid", sessionId);
+ Element x = child.addElement("x", "jabber:x:data");
+ x.addAttribute("type", "submit");
+ createFormTypeElement(x, "FORM_TYPE", "hidden", "http://jabber.org/protocol/admin");
+ createFormTypeElement(x, "accountjid", "jid-single",
+ commandParts[1] + "@" + xmppComponentConfig.getDomain());
+ createFormTypeElement(x, "password", "text-private", commandParts[2]);
+ userAdminCache.put(changeUserPasswordIq.getID(), inMsg);
+ externalComponentManager.sendPacket(this, changeUserPasswordIq);
+ logger.info("[sendChangeUserPasswordForm] -- SENT -- {}", changeUserPasswordIq);
+ } catch (Exception err) {
+ logger.error("[sendChangeUserPasswordForm] ", err);
}
- outMsg.setSubject(inMsg.getSubject());
- outMsg.setBody(inMsg.getBody());
- externalComponentManager.sendPacket(this, outMsg);
- logger.info("-- SENT -- {}", outMsg);
}
}