From 30bf0350b7ce31790b13efa48bdcfbdbb030ee6d Mon Sep 17 00:00:00 2001 From: Ng Yat Yan Date: Sun, 1 Dec 2024 12:14:47 +0800 Subject: [PATCH] V2.1.0 Attempts to integrate XEP-0133 --- pom.xml | 6 +- .../com/example/sshd/config/AppConfig.java | 9 + .../example/sshd/service/EchoComponent.java | 246 +++++++++++++++++- 3 files changed, 248 insertions(+), 13 deletions(-) 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); } }