From dd8a51e0f342c183116d8b2af3f12394b0ac53dd Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 17 Feb 2018 22:13:12 +0300 Subject: server: merge xmpp --- build.gradle | 2 - juick-server/build.gradle | 4 + .../src/main/java/com/juick/server/CleaningUp.java | 60 ++ .../src/main/java/com/juick/server/XMPPBot.java | 710 +++++++++++++++++++++ .../main/java/com/juick/server/XMPPConnection.java | 412 ++++++++++++ .../src/main/java/com/juick/server/XMPPServer.java | 407 ++++++++++++ .../src/main/java/com/juick/server/api/Index.java | 14 +- .../server/configuration/ApiAppConfiguration.java | 30 + .../com/juick/xmpp/extensions/JuickMessage.java | 162 +++++ .../java/com/juick/xmpp/extensions/JuickUser.java | 65 ++ .../java/com/juick/xmpp/helpers/JidConverter.java | 13 + .../java/com/juick/xmpp/helpers/XMPPStatus.java | 48 ++ .../java/com/juick/xmpp/s2s/BasicXmppSession.java | 69 ++ .../main/java/com/juick/xmpp/s2s/CacheEntry.java | 40 ++ .../main/java/com/juick/xmpp/s2s/Connection.java | 139 ++++ .../main/java/com/juick/xmpp/s2s/ConnectionIn.java | 216 +++++++ .../com/juick/xmpp/s2s/ConnectionListener.java | 14 + .../java/com/juick/xmpp/s2s/ConnectionOut.java | 167 +++++ .../main/java/com/juick/xmpp/s2s/DNSQueries.java | 65 ++ .../java/com/juick/xmpp/s2s/StanzaListener.java | 28 + .../com/juick/xmpp/s2s/util/DialbackUtils.java | 36 ++ juick-server/src/main/resources/juick.png | Bin 0 -> 2324 bytes .../com/juick/server/tests/XMPPServerTests.java | 190 ++++++ juick-xmpp-wip/build.gradle | 2 +- .../main/java/com/juick/components/XMPPRouter.java | 2 +- .../src/test/java/com/juick/xmpp/XMPPTests.java | 4 +- juick-xmpp/build.gradle | 29 - .../main/java/com/juick/components/CleaningUp.java | 60 -- .../main/java/com/juick/components/JuickBot.java | 710 --------------------- .../java/com/juick/components/XMPPConnection.java | 413 ------------ .../main/java/com/juick/components/XMPPServer.java | 409 ------------ .../configuration/XmppAppConfiguration.java | 70 -- .../components/configuration/XmppInitializer.java | 58 -- .../components/controllers/StatusController.java | 46 -- .../components/controllers/helpers/XMPPStatus.java | 48 -- .../com/juick/components/s2s/BasicXmppSession.java | 69 -- .../java/com/juick/components/s2s/CacheEntry.java | 40 -- .../java/com/juick/components/s2s/Connection.java | 139 ---- .../com/juick/components/s2s/ConnectionIn.java | 216 ------- .../juick/components/s2s/ConnectionListener.java | 14 - .../com/juick/components/s2s/ConnectionOut.java | 167 ----- .../java/com/juick/components/s2s/DNSQueries.java | 66 -- .../com/juick/components/s2s/StanzaListener.java | 28 - .../juick/components/s2s/util/DialbackUtils.java | 36 -- .../com/juick/xmpp/extensions/JuickMessage.java | 162 ----- .../java/com/juick/xmpp/extensions/JuickUser.java | 65 -- .../java/com/juick/xmpp/helpers/JidConverter.java | 13 - juick-xmpp/src/main/resources/juick.png | Bin 2324 -> 0 bytes juick-xmpp/src/main/webapp/WEB-INF/web.xml | 7 - .../com/juick/xmpp/server/XMPPServerTests.java | 189 ------ settings.gradle | 2 +- 51 files changed, 2893 insertions(+), 3062 deletions(-) create mode 100644 juick-server/src/main/java/com/juick/server/CleaningUp.java create mode 100644 juick-server/src/main/java/com/juick/server/XMPPBot.java create mode 100644 juick-server/src/main/java/com/juick/server/XMPPConnection.java create mode 100644 juick-server/src/main/java/com/juick/server/XMPPServer.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/extensions/JuickMessage.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/extensions/JuickUser.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/helpers/JidConverter.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/helpers/XMPPStatus.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/BasicXmppSession.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/CacheEntry.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/Connection.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionIn.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionListener.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionOut.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/DNSQueries.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/StanzaListener.java create mode 100644 juick-server/src/main/java/com/juick/xmpp/s2s/util/DialbackUtils.java create mode 100644 juick-server/src/main/resources/juick.png create mode 100644 juick-server/src/test/java/com/juick/server/tests/XMPPServerTests.java delete mode 100644 juick-xmpp/build.gradle delete mode 100644 juick-xmpp/src/main/java/com/juick/components/CleaningUp.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/JuickBot.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/XMPPConnection.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/XMPPServer.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/configuration/XmppAppConfiguration.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/configuration/XmppInitializer.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/controllers/StatusController.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/controllers/helpers/XMPPStatus.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/BasicXmppSession.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/CacheEntry.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/Connection.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionIn.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionListener.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionOut.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/DNSQueries.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/StanzaListener.java delete mode 100644 juick-xmpp/src/main/java/com/juick/components/s2s/util/DialbackUtils.java delete mode 100644 juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickMessage.java delete mode 100644 juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickUser.java delete mode 100644 juick-xmpp/src/main/java/com/juick/xmpp/helpers/JidConverter.java delete mode 100644 juick-xmpp/src/main/resources/juick.png delete mode 100644 juick-xmpp/src/main/webapp/WEB-INF/web.xml delete mode 100644 juick-xmpp/src/test/java/com/juick/xmpp/server/XMPPServerTests.java diff --git a/build.gradle b/build.gradle index 5b3155b8..a5d2e440 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,6 @@ apply plugin: 'org.akhikhl.gretty' farm { webapp ':juick-www' - webapp ':juick-xmpp' webapp ':juick-server' @@ -59,7 +58,6 @@ repositories { dependencies { testCompile project(':juick-core') testCompile project(':juick-www') - testCompile project(':juick-xmpp') testCompile "org.json:json:20180130" testCompile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" diff --git a/juick-server/build.gradle b/juick-server/build.gradle index f0d29f98..5710ef21 100644 --- a/juick-server/build.gradle +++ b/juick-server/build.gradle @@ -20,6 +20,10 @@ dependencies { compile 'org.springframework.social:spring-social-twitter:1.1.2.RELEASE' compile 'org.apache.commons:commons-email:1.5' compile 'org.imgscalr:imgscalr-lib:4.2' + compile ('com.github.juick:com.juick.xmpp:658f8cf751') { + exclude group: 'xmlpull' + } + providedCompile 'xpp3:xpp3:1.1.4c' testCompile project(path: ':juick-core', configuration: 'testArtifacts') testCompile project(path: ':juick-server-web', configuration: 'testArtifacts') diff --git a/juick-server/src/main/java/com/juick/server/CleaningUp.java b/juick-server/src/main/java/com/juick/server/CleaningUp.java new file mode 100644 index 00000000..6acd4bba --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/CleaningUp.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.time.Duration; +import java.time.Instant; + +/** + * + * @author ugnich + */ +@Component +public class CleaningUp { + + private static final Logger logger = LoggerFactory.getLogger(CleaningUp.class); + + private static final int TIMEOUT_MINUTES = 15; + + @Inject + XMPPServer xmpp; + + @Scheduled(fixedDelay = 10000) + public void cleanUp() { + Instant now = Instant.now(); + xmpp.getOutConnections().keySet().stream().filter(c -> Duration.between(now, c.getUpdated()).toMinutes() > TIMEOUT_MINUTES) + .forEach(c -> { + logger.info("closing idle outgoing connection to {}", c.to); + c.logoff(); + xmpp.getOutConnections().remove(c); + }); + + xmpp.getInConnections().stream().filter(c -> Duration.between(now, c.updated).toMinutes() > TIMEOUT_MINUTES) + .forEach(c -> { + logger.info("closing idle incoming connection from {}", c.from); + c.closeConnection(); + xmpp.getInConnections().remove(c); + }); + } +} diff --git a/juick-server/src/main/java/com/juick/server/XMPPBot.java b/juick-server/src/main/java/com/juick/server/XMPPBot.java new file mode 100644 index 00000000..21138607 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/XMPPBot.java @@ -0,0 +1,710 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.server; + +import com.juick.Tag; +import com.juick.User; +import com.juick.xmpp.s2s.StanzaListener; +import com.juick.formatters.PlainTextFormatter; +import com.juick.server.helpers.TagStats; +import com.juick.server.protocol.annotation.UserCommand; +import com.juick.service.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.ocpsoft.prettytime.PrettyTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.*; +import rocks.xmpp.core.stanza.model.client.ClientMessage; +import rocks.xmpp.core.stanza.model.client.ClientPresence; +import rocks.xmpp.core.stanza.model.errors.Condition; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * + * @author ugnich + */ +@Component +public class XMPPBot implements StanzaListener, AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(XMPPBot.class); + + @Inject + private XMPPServer xmpp; + @Inject + private XMPPConnection router; + @Value("${xmppbot_jid}") + private Jid jid; + + private PrettyTime pt; + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private ShowQueriesService showQueriesService; + @Inject + private PrivacyQueriesService privacyQueriesService; + @Inject + private SubscriptionService subscriptionService; + + @PostConstruct + public void init() { + xmpp.addStanzaListener(this); + broadcastPresence(null); + pt = new PrettyTime(new Locale("ru")); + } + + public Jid getJid() { + return jid; + } + + public boolean incomingPresence(Presence p) { + final String username = p.getTo().getLocal(); + final boolean toJuick = username.equals(jid.getLocal()); + + if (p.getType() == null) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().asBareJid()); + reply.setTo(p.getFrom().asBareJid()); + reply.setType(Presence.Type.UNSUBSCRIBE); + xmpp.sendOut(ClientPresence.from(reply)); + return true; + } else if (p.getType().equals(Presence.Type.PROBE)) { + int uid_to = 0; + if (!toJuick) { + uid_to = userService.getUIDbyName(username); + } + + if (toJuick || uid_to > 0) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().withResource(jid.getResource())); + reply.setTo(p.getFrom()); + reply.setPriority((byte)10); + if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { + reply.setStatus("Send ON to enable notifications"); + } + xmpp.sendOut(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + xmpp.sendOut(ClientPresence.from(reply)); + return true; + } + return true; + } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { + boolean canSubscribe = false; + if (toJuick) { + canSubscribe = true; + } else { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + canSubscribe = true; + } + } + + if (canSubscribe) { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.SUBSCRIBED); + xmpp.sendOut(ClientPresence.from(reply)); + + reply.setFrom(reply.getFrom().withResource(jid.getResource())); + reply.setPriority((byte) 10); + reply.setType(null); + xmpp.sendOut(ClientPresence.from(reply)); + + return true; + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + xmpp.sendOut(ClientPresence.from(reply)); + return true; + } + } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { + if (!toJuick) { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + } + } + + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.UNSUBSCRIBED); + xmpp.sendOut(ClientPresence.from(reply)); + } + + return false; + } + + public boolean incomingMessage(Message msg) { + if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { + StanzaError error = msg.getError(); + if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { + // offline query is full, deactivating this jid + if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { + logger.info("{} is inactive now", msg.getFrom()); + return true; + } + } + return false; + } + if (StringUtils.isBlank(msg.getBody())) { + return false; + } + String username = msg.getTo().getLocal(); + + User user_from; + String signuphash = StringUtils.EMPTY; + user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); + if (user_from == null) { + signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); + } + + if (user_from == null) { + Message reply = new Message(); + reply.setFrom(msg.getTo()); + reply.setTo(msg.getFrom()); + reply.setType(Message.Type.CHAT); + if (username.equals(jid.getLocal())) { + reply.setBody("Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); + } else { + reply.setBody("Внимание, системное сообщение!\nВаш JabberID не обнаружен в списке доверенных. Для того, чтобы отправить сообщение пользователю " + username + "@juick.com, пожалуйста зарегистрируйте свой JabberID в системе: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nWarning, system message!\nYour JabberID is not found in our server's white list. To send a message to " + username + "@juick.com, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); + } + xmpp.sendOut(ClientMessage.from(reply)); + return true; + } + + if (username.equals(jid.getLocal())) { + try { + return incomingMessageJuick(user_from, msg); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + return false; + } + } + + int uid_to = userService.getUIDbyName(username); + + if (uid_to == 0) { + Message reply = new Message(); + reply.setFrom(msg.getTo()); + reply.setTo(msg.getFrom()); + reply.setType(Message.Type.ERROR); + reply.setId(msg.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + xmpp.sendOut(ClientMessage.from(reply)); + return true; + } + + boolean success = false; + if (!userService.isInBLAny(uid_to, user_from.getUid())) { + success = pmQueriesService.createPM(user_from.getUid(), uid_to, msg.getBody()); + } + + if (success) { + Message m = new Message(); + m.setFrom(jid.asBareJid()); + m.setTo(Jid.of(Integer.toString(uid_to), "push.juick.com", null)); + com.juick.Message jmsg = new com.juick.Message(); + jmsg.setUser(user_from); + jmsg.setText(msg.getBody()); + m.addExtension(jmsg); + router.sendStanza(m); + + m.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); + router.sendStanza(m); + + List jids; + boolean inroster = false; + jids = userService.getJIDsbyUID(uid_to); + for (String userJid : jids) { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + inroster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); + if (inroster) { + mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(msg.getBody()); + } else { + mm.setFrom(jid); + mm.setBody("Private message from @" + jmsg.getUser().getName() + ":\n" + msg.getBody()); + } + xmpp.sendOut(ClientMessage.from(mm)); + } + } else { + Message reply = new Message(); + reply.setFrom(msg.getTo()); + reply.setTo(msg.getFrom()); + reply.setType(Message.Type.ERROR); + reply.setId(msg.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.NOT_ALLOWED)); + xmpp.sendOut(ClientMessage.from(reply)); + } + + return false; + } + + public Optional processCommand(User user, Jid from, String input) throws InvocationTargetException, + IllegalAccessException, NoSuchMethodException { + Optional cmd = MethodUtils.getMethodsListWithAnnotation(getClass(), UserCommand.class).stream() + .filter(m -> Pattern.compile(m.getAnnotation(UserCommand.class).pattern(), + m.getAnnotation(UserCommand.class).patternFlags()).matcher(input).matches()) + .findFirst(); + if (cmd.isPresent()) { + Matcher matcher = Pattern.compile(cmd.get().getAnnotation(UserCommand.class).pattern(), + cmd.get().getAnnotation(UserCommand.class).patternFlags()).matcher(input); + List groups = new ArrayList<>(); + while (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + groups.add(matcher.group(i)); + } + } + return Optional.of((String) getClass().getMethod(cmd.get().getName(), User.class, Jid.class, String[].class) + .invoke(this, user, from, groups.toArray(new String[groups.size()]))); + } + return Optional.empty(); + } + public boolean incomingMessageJuick(User user_from, Message msg) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { + String command = msg.getBody().trim(); + int commandlen = command.length(); + + // COMPATIBILITY + if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { + command = command.substring(3).trim(); + } + + Optional result = processCommand(user_from, msg.getFrom(), command); + result.ifPresent(r -> sendReply(msg.getFrom(), r)); + return result.isPresent(); + } + + @UserCommand(pattern = "^ping$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "PING - returns you a PONG") + public String commandPing(User user, Jid from, String[] input) { + Presence p = new Presence(from); + p.setFrom(jid); + p.setPriority((byte) 10); + xmpp.sendOut(ClientPresence.from(p)); + return "PONG"; + } + + @UserCommand(pattern = "^help$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "HELP - returns this help message") + public String commandHelp(User user, Jid from, String[] input) { + return Arrays.stream(getClass().getDeclaredMethods()) + .filter(m -> m.isAnnotationPresent(UserCommand.class)) + .map(m -> m.getAnnotation(UserCommand.class).help()) + .collect(Collectors.joining("\n")); + } + + @UserCommand(pattern = "^login$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "LOGIN - log in to Juick website") + public String commandLogin(User user_from, Jid from, String[] input) { + return "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); + } + @UserCommand(pattern = "^\\@(\\S+)\\s+([\\s\\S]+)$", help = "@username message - send PM to username") + public String commandPM(User user_from, Jid from, String... arguments) { + String body = arguments[1]; + int ret = 0; + + User user_to = userService.getUserByName(arguments[0]); + List jids_to = null; + boolean haveInRoster = false; + + if (user_to.getUid() > 0) { + if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) { + if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) { + jids_to = userService.getJIDsbyUID(user_to.getUid()); + ret = 200; + } else { + ret = 500; + } + } else { + ret = 403; + } + } else { + ret = 404; + } + + if (ret == 200) { + Message msg = new Message(); + msg.setFrom(jid.asBareJid()); + msg.setTo(Jid.of(Integer.toString(user_to.getUid()), "push.juick.com", null)); + com.juick.Message jmsg = new com.juick.Message(); + jmsg.setUser(user_from); + jmsg.setTo(user_to); + jmsg.setText(body); + msg.addExtension(jmsg); + router.sendStanza(msg); + + msg.setTo(Jid.of(Integer.toString(user_to.getUid()), "ws.juick.com", null)); + router.sendStanza(msg); + + for (String userJid : jids_to) { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + haveInRoster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); + if (haveInRoster) { + mm.setFrom(Jid.of(user_from.getName(), "juick.com", "Juick")); + mm.setBody(body); + } else { + mm.setFrom(jid); + mm.setBody("Private message from @" + user_from.getName() + ":\n" + body); + } + xmpp.sendOut(ClientMessage.from(mm)); + } + } + + if (ret == 200) { + return "Private message sent"; + } else { + return "Error " + ret; + } + } + @UserCommand(pattern = "^bl$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL - Show your blacklist") + public String commandBLShow(User user_from, Jid from, String... arguments) { + List blusers = userService.getUserBLUsers(user_from.getUid()); + List bltags = tagService.getUserBLTags(user_from.getUid()); + + String txt = StringUtils.EMPTY; + if (bltags.size() > 0) { + for (String bltag : bltags) { + txt += "*" + bltag + "\n"; + } + + if (blusers.size() > 0) { + txt += "\n"; + } + } + if (blusers.size() > 0) { + for (User bluser : blusers) { + txt += "@" + bluser.getName() + "\n"; + } + } + if (txt.isEmpty()) { + txt = "You don't have any users or tags in your blacklist."; + } + + return txt; + } + + @UserCommand(pattern = "^#\\+$", help = "#+ - Show last Juick messages") + public String commandLast(User user_from, Jid from, String... arguments) { + return "Last messages:\n" + + printMessages(messagesService.getAll(user_from.getUid(), 0), true); + } + + @UserCommand(pattern = "@", help = "@ - Show recommendations and popular personal blogs") + public String commandUsers(User user_from, Jid from, String... arguments) { + StringBuilder msg = new StringBuilder(); + msg.append("Recommended blogs"); + List recommendedUsers = showQueriesService.getRecommendedUsers(user_from); + if (recommendedUsers.size() > 0) { + for (String user : recommendedUsers) { + msg.append("\n@").append(user); + } + } else { + msg.append("\nNo recommendations now. Subscribe to more blogs. ;)"); + } + msg.append("\n\nTop 10 personal blogs:"); + List topUsers = showQueriesService.getTopUsers(); + if (topUsers.size() > 0) { + for (String user : topUsers) { + msg.append("\n@").append(user); + } + } else { + msg.append("\nNo top users. Empty DB? ;)"); + } + return msg.toString(); + } + @UserCommand(pattern = "^bl\\s+@([^\\s\\n\\+]+)", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL @username - add @username to your blacklist") + public String blacklistUser(User user_from, Jid from, String... arguments) { + User blUser = userService.getUserByName(arguments[0]); + if (blUser != null) { + PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistUser(user_from, blUser); + if (result == PrivacyQueriesService.PrivacyResult.Added) { + return "User added to your blacklist"; + } else { + return "User removed from your blacklist"; + } + } + return "User not found"; + } + @UserCommand(pattern = "^bl\\s\\*(\\S+)$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL *tag - add *tag to your blacklist") + public String blacklistTag(User user_from, Jid from, String... arguments) { + User blUser = userService.getUserByName(arguments[0]); + if (blUser != null) { + Tag tag = tagService.getTag(arguments[0], false); + if (tag != null) { + PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistTag(user_from, tag); + if (result == PrivacyQueriesService.PrivacyResult.Added) { + return "Tag added to your blacklist"; + } else { + return "Tag removed from your blacklist"; + } + } + } + return "Tag not found"; + } + @UserCommand(pattern = "\\*", help = "* - Show your tags") + public String commandTags(User currentUser, Jid from, String... args) { + List tags = tagService.getUserTagStats(currentUser.getUid()); + String msg = "Your tags: (tag - messages)\n" + + tags.stream() + .map(t -> String.format("\n*%s - %d", t.getTag().getName(), t.getUsageCount())).collect(Collectors.joining()); + return msg; + } + @UserCommand(pattern = "S", help = "S - Show your subscriptions") + public String commandSubscriptions(User currentUser, Jid from, String... args) { + List friends = userService.getUserFriends(currentUser.getUid()); + List tags = subscriptionService.getSubscribedTags(currentUser); + String msg = friends.size() > 0 ? "You are subscribed to users:" + friends.stream().map(u -> "\n@" + u.getName()) + .collect(Collectors.joining()) + : "You are not subscribed to any user."; + msg += tags.size() > 0 ? "\nYou are subscribed to tags:" + tags.stream().map(t -> "\n*" + t) + .collect(Collectors.joining()) + : "\nYou are not subscribed to any tag."; + return msg; + } + @UserCommand(pattern = "!", help = "! - Show your favorite messages") + public String commandFavorites(User currentUser, Jid from, String... args) { + List mids = messagesService.getUserRecommendations(currentUser.getUid(), 0); + if (mids.size() > 0) { + return "Favorite messages: \n" + printMessages(mids, false); + } + return "No favorite messages, try to \"like\" something ;)"; + } + // TODO: target notification + @UserCommand(pattern = "^(s|u)\\s+\\@(\\S+)$", help = "S @username - subscribe to user" + + "\nU @username - unsubscribe from user", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandSubscribeUser(User user, Jid from, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + User toUser = userService.getUserByName(args[1]); + if (subscribe) { + if (subscriptionService.subscribeUser(user, toUser)) { + return "Subscribed to @" + toUser.getName(); + } + } else { + if (subscriptionService.unSubscribeUser(user, toUser)) { + return "Unsubscribed from @" + toUser.getName(); + } + return "You was not subscribed to @" + toUser.getName(); + } + return "Error"; + } + @UserCommand(pattern = "^(s|u)\\s+\\*(\\S+)$", help = "S *tag - subscribe to tag" + + "\nU *tag - unsubscribe from tag", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandSubscribeTag(User user, Jid from, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + Tag tag = tagService.getTag(args[1], true); + if (subscribe) { + if (subscriptionService.subscribeTag(user, tag)) { + return "Subscribed"; + } + } else { + if (subscriptionService.unSubscribeTag(user, tag)) { + return "Unsubscribed from " + tag.getName(); + } + return "You was not subscribed to " + tag.getName(); + } + return "Error"; + } + @UserCommand(pattern = "^(s|u)\\s+#(\\d+)$", help = "S #1234 - subscribe to comments" + + "\nU #1234 - unsubscribe from comments", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandSubscribeMessage(User user, Jid from, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + int mid = NumberUtils.toInt(args[1], 0); + if (messagesService.getMessage(mid) != null) { + if (subscribe) { + if (subscriptionService.subscribeMessage(mid, user.getUid())) { + return "Subscribed"; + } + } else { + if (subscriptionService.unSubscribeMessage(mid, user.getUid())) { + return "Unsubscribed from #" + mid; + } + return "You was not subscribed to #" + mid; + } + } + return "Error"; + } + @UserCommand(pattern = "^(on|off)$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "ON/OFF - Enable/disable subscriptions delivery") + public String commandOnOff(User user, Jid from, String[] input) { + UserService.ActiveStatus newStatus; + String retValUpdated; + if (input[0].toLowerCase().equals("on")) { + newStatus = UserService.ActiveStatus.Active; + retValUpdated = "Notifications are activated for " + from.asBareJid().toEscapedString(); + } else { + newStatus = UserService.ActiveStatus.Inactive; + retValUpdated = "Notifications are disabled for " + from.asBareJid().toEscapedString(); + } + + if (userService.setActiveStatusForJID(from.asBareJid().toEscapedString(), newStatus)) { + return retValUpdated; + } else { + return String.format("Subscriptions status for %s was not changed", from.toEscapedString()); + } + } + @UserCommand(pattern = "^\\@([^\\s\\n\\+]+)(\\+?)$", + help = "@username+ - Show user's info and last 20 messages") + public String commandUser(User user, Jid from, String... arguments) { + User blogUser = userService.getUserByName(arguments[0]); + int page = arguments[1].length(); + if (blogUser.getUid() > 0) { + List mids = messagesService.getUserBlog(blogUser.getUid(), 0, 0); + return String.format("Last messages from @%s:\n%s", arguments[0], + printMessages(mids, false)); + } + return "User not found"; + } + @UserCommand(pattern = "^#(\\d+)(\\+?)$", help = "#1234 - Show message (#1234+ - message with replies)") + public String commandShow(User user, Jid from, String... arguments) { + boolean showReplies = arguments[1].length() > 0; + int mid = NumberUtils.toInt(arguments[0], 0); + if (mid == 0) { + return "Error"; + } + com.juick.Message msg = messagesService.getMessage(mid); + if (msg != null) { + if (showReplies) { + List replies = messagesService.getReplies(mid); + replies.add(0, msg); + return String.join("\n", + replies.stream().map(PlainTextFormatter::formatPostSummary).collect(Collectors.toList())); + } + return PlainTextFormatter.formatPostSummary(msg); + } + return "Message not found"; + } + @UserCommand(pattern = "^D #(\\d+)$", help = "D #1234 - Delete post", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandDeletePost(User user, Jid from, String... args) { + int mid = Integer.valueOf(args[0]); + if (messagesService.deleteMessage(user.getUid(), mid)) { + return "Message deleted"; + } + return "This is not your message"; + } + @UserCommand(pattern = "^D #(\\d+)(\\.|\\-|\\/)(\\d+)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandDeleteReply(User user, Jid from, String... args) { + int mid = Integer.valueOf(args[0]); + int rid = Integer.valueOf(args[2]); + if (messagesService.deleteReply(user.getUid(), mid, rid)) { + return "Reply deleted"; + } else { + return "This is not your reply"; + } + } + /* + @UserCommand(pattern = "(^(D L|DL|D LAST)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE) + public String commandDeleteLast(User user, Jid from, String... args) { + + } + */ + void sendReply(Jid jidTo, String txt) { + Message reply = new Message(); + reply.setFrom(jid); + reply.setTo(jidTo); + reply.setType(Message.Type.CHAT); + reply.setBody(txt); + xmpp.sendOut(ClientMessage.from(reply)); + } + + void sendNotification(Stanza stanza) { + xmpp.sendOut(stanza); + } + + @Override + public void stanzaReceived(Stanza xmlValue) { + if (xmlValue instanceof Presence) { + Presence p = (Presence) xmlValue; + if (p.getType() == null || !p.getType().equals(Presence.Type.ERROR)) { + incomingPresence(p); + } + } else if (xmlValue instanceof Message) { + Message msg = (Message) xmlValue; + if (!incomingMessage(msg)) { + router.sendStanza(msg); + } + } else if (xmlValue instanceof IQ) { + IQ iq = (IQ) xmlValue; + router.sendStanza(iq); + } + } + + String printMessages(List mids, boolean crop) { + return messagesService.getMessages(mids).stream() + .sorted(Collections.reverseOrder()) + .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); + } + + void broadcastPresence(Presence.Type type) { + Presence presence = new Presence(); + presence.setFrom(jid); + if (type != null) { + presence.setType(type); + } + userService.getActiveJIDs().forEach(j -> { + try { + presence.setTo(Jid.of(j)); + xmpp.sendOut(ClientPresence.from(presence)); + } catch (IllegalArgumentException ex) { + logger.warn("Invalid jid: {}", j, ex); + } + }); + } + + @Override + public void close() throws Exception { + broadcastPresence(Presence.Type.UNAVAILABLE); + } +} diff --git a/juick-server/src/main/java/com/juick/server/XMPPConnection.java b/juick-server/src/main/java/com/juick/server/XMPPConnection.java new file mode 100644 index 00000000..f5448880 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/XMPPConnection.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.server; + +import com.juick.Attachment; +import com.juick.User; +import com.juick.xmpp.s2s.BasicXmppSession; +import com.juick.server.helpers.UserInfo; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.UserService; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.stanza.AbstractIQHandler; +import rocks.xmpp.core.stanza.model.IQ; +import rocks.xmpp.core.stanza.model.Message; +import rocks.xmpp.core.stanza.model.Stanza; +import rocks.xmpp.core.stanza.model.client.ClientMessage; +import rocks.xmpp.core.stanza.model.errors.Condition; +import rocks.xmpp.extensions.caps.model.EntityCapabilities; +import rocks.xmpp.extensions.component.accept.ExternalComponent; +import rocks.xmpp.extensions.filetransfer.FileTransfer; +import rocks.xmpp.extensions.filetransfer.FileTransferManager; +import rocks.xmpp.extensions.nick.model.Nickname; +import rocks.xmpp.extensions.oob.model.x.OobX; +import rocks.xmpp.extensions.ping.PingManager; +import rocks.xmpp.extensions.vcard.temp.model.VCard; +import rocks.xmpp.extensions.version.SoftwareVersionManager; +import rocks.xmpp.extensions.version.model.SoftwareVersion; +import rocks.xmpp.util.XmppUtils; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +/** + * @author ugnich + */ +@Component +public class XMPPConnection implements AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(XMPPConnection.class); + + private ExternalComponent router; + @Inject + private XMPPBot bot; + + @Value("${componentname:localhost}") + private String componentName; + @Value("${component_port:5347}") + private int componentPort; + @Value("${xmpp_password:secret}") + private String password; + @Value("${upload_tmp_dir:/tmp}") + private String tmpDir; + + @Inject + public MessagesService messagesService; + @Inject + public UserService userService; + @Inject + public SubscriptionService subscriptionService; + @Inject + private BasicXmppSession session; + @Inject + private ExecutorService service; + + @PostConstruct + public void init() { + logger.info("stream router start connecting to {}", componentPort); + router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort); + PingManager pingManager = router.getManager(PingManager.class); + pingManager.setEnabled(true); + router.disableFeature(EntityCapabilities.NAMESPACE); + SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); + softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", System.getProperty("os.name", "generic"))); + VCard vCard = new VCard(); + vCard.setFormattedName("Juick"); + try { + vCard.setUrl(new URL("http://juick.com/")); + vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray(getClass().getClassLoader().getResource("juick.png")))); + } catch (MalformedURLException e) { + logger.error("invalid url", e); + } catch (IOException e) { + logger.warn("invalid resource", e); + } + router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { + @Override + protected IQ processRequest(IQ iq) { + if (iq.getTo().equals(bot.getJid()) || iq.getTo().asBareJid().equals(bot.getJid().asBareJid()) + || iq.getTo().asBareJid().toEscapedString().equals(bot.getJid().getDomain())) { + return iq.createResult(vCard); + } + User user = userService.getUserByName(iq.getTo().getLocal()); + if (user.getUid() > 0) { + UserInfo info = userService.getUserInfo(user); + VCard userVCard = new VCard(); + userVCard.setFormattedName(info.getFullName()); + userVCard.setNickname(user.getName()); + try { + userVCard.setPhoto(new VCard.Image(new URI("http://i.juick.com/a/" + user.getUid() + ".png"))); + if (info.getUrl() != null) { + userVCard.setUrl(new URL(info.getUrl())); + } + } catch (MalformedURLException | URISyntaxException e) { + logger.warn("url exception", e); + } + return iq.createResult(userVCard); + } + return iq.createError(Condition.BAD_REQUEST); + } + }); + router.addInboundMessageListener(e -> { + Message message = e.getMessage(); + Jid jid = message.getTo(); + if (jid.getDomain().equals(router.getDomain().toEscapedString())) { + com.juick.Message jmsg = message.getExtension(com.juick.Message.class); + if (jmsg != null) { + if (jid.getLocal().equals("recomm")) { + sendJuickRecommendation(jmsg); + } else { + if (jmsg.getRid() > 0) { + sendJuickComment(jmsg); + } else if (jmsg.getMid() > 0) { + sendJuickMessage(jmsg); + } + } + } + } else if (jid.getDomain().endsWith(bot.getJid().getDomain()) && (jid.getDomain().equals(bot.getJid().getDomain()) + || jid.getDomain().endsWith("." + bot.getJid().getDomain()))) { + if (logger.isInfoEnabled()) { + try { + logger.info("unhandled message: {}", stanzaToString(message)); + } catch (JAXBException | XMLStreamException ex) { + logger.error("JAXB exception", ex); + } + } + } else { + route(ClientMessage.from(message)); + } + }); + router.addInboundIQListener(e -> { + IQ iq = e.getIQ(); + Jid jid = iq.getTo(); + if (!jid.getDomain().equals(bot.getJid().getDomain())) { + route(iq); + } + }); + FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); + fileTransferManager.addFileTransferOfferListener(e -> { + try { + List allowedTypes = new ArrayList() {{ + add("png"); + add("jpg"); + }}; + String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); + String targetFilename = String.format("%s.%s", + DigestUtils.md5Hex(String.format("%s-%s", + e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); + if (allowedTypes.contains(attachmentExtension)) { + Path filePath = Paths.get(tmpDir, targetFilename); + FileTransfer ft = e.accept(filePath).get(); + ft.addFileTransferStatusListener(st -> { + logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); + if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { + logger.info("transfer completed"); + Message msg = new Message(); + msg.setType(Message.Type.CHAT); + msg.setFrom(e.getInitiator()); + msg.setTo(bot.getJid()); + msg.setBody(e.getDescription()); + try { + String attachmentUrl = String.format("juick://%s", targetFilename); + msg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); + router.sendMessage(msg); + } catch (URISyntaxException e1) { + logger.warn("attachment error", e1); + } + } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { + logger.info("transfer failed", ft.getException()); + Message msg = new Message(); + msg.setType(Message.Type.CHAT); + msg.setFrom(bot.getJid()); + msg.setTo(e.getInitiator()); + msg.setBody("File transfer failed, please report to us"); + router.sendMessage(msg); + } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { + logger.info("transfer cancelled"); + } + }); + ft.transfer(); + logger.info("transfer started"); + } else { + e.reject(); + logger.info("transfer rejected"); + } + } catch (IOException | InterruptedException | ExecutionException e1) { + logger.error("ft error", e1); + } + }); + router.addConnectionListener(event -> { + if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { + logger.info("component connected"); + } + }); + service.submit(() -> { + try { + Thread.sleep(3000); + router.connect(); + } catch (InterruptedException | XmppException e) { + logger.warn("xmpp exception", e); + } + }); + } + + String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { + StringWriter stanzaWriter = new StringWriter(); + XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( + router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); + router.createMarshaller().marshal(stanza, xmppStreamWriter); + xmppStreamWriter.flush(); + xmppStreamWriter.close(); + return stanzaWriter.toString(); + } + + void route(Stanza stanza) { + try { + String xml = stanzaToString(stanza); + logger.info("stream router (out): {}", xml); + bot.sendNotification(stanza); + } catch (XMLStreamException | JAXBException e) { + logger.error("JAXB exception", e); + } + + } + + public void sendStanza(Stanza xml) { + router.send(xml); + } + + + + public void sendJuickMessage(com.juick.Message jmsg) { + List jids = new ArrayList<>(); + + if (jmsg.FriendsOnly) { + jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly); + } else { + List users = subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()); + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + jids.add(jid); + } + } + } + com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()); + String txt = "@" + jmsg.getUser().getName() + ":" + fullMsg.getTagsString() + "\n"; + Attachment attachment = fullMsg.getAttachment(); + if (attachment != null) { + txt += attachment.getMedium().getUrl() + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid() + " http://juick.com/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(bot.getJid()); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (attachment != null) { + try { + OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + for (String jid : jids) { + msg.setTo(Jid.of(jid)); + route(ClientMessage.from(msg)); + } + } + + public void sendJuickComment(com.juick.Message jmsg) { + List users; + String replyQuote; + String replyTo; + + users = subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()); + com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) + : messagesService.getMessage(jmsg.getMid()); + replyTo = replyMessage.getUser().getName(); + com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + replyQuote = fullReply.getReplyQuote(); + + String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; + Attachment attachment = jmsg.getAttachment(); + if (attachment != null) { + txt += attachment.getMedium().getUrl() + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/" + jmsg.getMid() + "#" + jmsg.getRid(); + + Message msg = new Message(); + msg.setFrom(bot.getJid()); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.addExtension(jmsg); + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + route(ClientMessage.from(msg)); + } + } + } + + public void sendJuickRecommendation(com.juick.Message recomm) { + List users; + com.juick.Message jmsg = messagesService.getMessage(recomm.getMid()); + users = subscriptionService.getUsersSubscribedToUserRecommendations(recomm.getUser().getUid(), + recomm.getMid(), jmsg.getUser().getUid()); + + String txt = "Recommended by @" + recomm.getUser().getName() + ":\n"; + txt += "@" + jmsg.getUser().getName() + ":" + jmsg.getTagsString() + "\n"; + Attachment attachment = jmsg.getAttachment(); + if (attachment != null) { + txt += attachment.getMedium().getUrl() + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid(); + if (jmsg.getReplies() > 0) { + if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { + txt += " (" + jmsg.getReplies() + " reply)"; + } else { + txt += " (" + jmsg.getReplies() + " replies)"; + } + } + txt += " http://juick.com/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(bot.getJid()); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (attachment != null) { + try { + OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + route(ClientMessage.from(msg)); + } + } + } + + @Override + public void close() throws Exception { + if (router != null) { + router.close(); + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/XMPPServer.java b/juick-server/src/main/java/com/juick/server/XMPPServer.java new file mode 100644 index 00000000..01437232 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/XMPPServer.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.server; + +import com.juick.xmpp.s2s.*; +import com.juick.service.UserService; +import com.juick.xmpp.extensions.JuickMessage; +import com.juick.xmpp.extensions.StreamError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.xmlpull.v1.XmlPullParserException; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.Stanza; +import rocks.xmpp.util.XmppUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.net.ssl.*; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.*; +import java.net.*; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author ugnich + */ +@Component +public class XMPPServer implements ConnectionListener, AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(XMPPServer.class); + + @Inject + public ExecutorService service; + @Value("${hostname}") + private Jid jid; + @Value("${s2s_port:5269}") + private int s2sPort; + @Value("${keystore}") + public String keystore; + @Value("${keystore_password}") + public String keystorePassword; + @Value("${broken_ssl_hosts}") + public String[] brokenSSLhosts; + @Value("${banned_hosts}") + public String[] bannedHosts; + + private final List inConnections = new CopyOnWriteArrayList<>(); + private final Map> outConnections = new ConcurrentHashMap<>(); + private final List outCache = new CopyOnWriteArrayList<>(); + private final List stanzaListeners = new CopyOnWriteArrayList<>(); + private final AtomicBoolean closeFlag = new AtomicBoolean(false); + + SSLContext sc; + private TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + + + private ServerSocket listener; + + @Inject + private BasicXmppSession session; + @Inject + private UserService userService; + + @PostConstruct + public void init() throws KeyStoreException { + closeFlag.set(false); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream ksIs = new FileInputStream(keystore)) { + ks.load(ksIs, keystorePassword.toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory + .getDefaultAlgorithm()); + kmf.init(ks, keystorePassword.toCharArray()); + sc = SSLContext.getInstance("TLSv1.2"); + sc.init(kmf.getKeyManagers(), trustAllCerts, new SecureRandom()); + } catch (Exception e) { + logger.warn("tls unavailable"); + } + service.submit(() -> { + try { + listener = new ServerSocket(s2sPort); + logger.info("s2s listener ready"); + while (!listener.isClosed()) { + if (Thread.currentThread().isInterrupted()) break; + Socket socket = listener.accept(); + ConnectionIn client = new ConnectionIn(this, socket); + addConnectionIn(client); + service.submit(client); + } + } catch (SocketException e) { + // shutdown + } catch (IOException | XmlPullParserException e) { + logger.warn("xmpp exception", e); + } + }); + } + + @Override + public void close() throws Exception { + if (!listener.isClosed()) { + listener.close(); + } + outConnections.forEach((c, s) -> { + c.logoff(); + outConnections.remove(c); + }); + inConnections.forEach(c -> { + c.closeConnection(); + inConnections.remove(c); + }); + if (!listener.isClosed()) { + listener.close(); + } + service.shutdown(); + logger.info("XMPP server destroyed"); + } + + public void addConnectionIn(ConnectionIn c) { + c.setListener(this); + inConnections.add(c); + } + + public void addConnectionOut(ConnectionOut c, Optional socket) { + c.setListener(this); + outConnections.put(c, socket); + } + + public void removeConnectionIn(ConnectionIn c) { + inConnections.remove(c); + } + + public void removeConnectionOut(ConnectionOut c) { + outConnections.remove(c); + } + + public String getFromCache(Jid to) { + final String[] cache = new String[1]; + outCache.stream().filter(c -> c.hostname != null && c.hostname.equals(to)).findFirst().ifPresent(c -> { + cache[0] = c.xml; + outCache.remove(c); + }); + return cache[0]; + } + + public Optional getConnectionOut(Jid hostname, boolean needReady) { + return outConnections.keySet().stream().filter(c -> c.to != null && + c.to.equals(hostname) && (!needReady || c.streamReady)).findFirst(); + } + + public Optional getConnectionIn(String streamID) { + return inConnections.stream().filter(c -> c.streamID != null && c.streamID.equals(streamID)).findFirst(); + } + + public void sendOut(Stanza s) { + try { + StringWriter stanzaWriter = new StringWriter(); + XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( + session.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); + session.createMarshaller().marshal(s, xmppStreamWriter); + xmppStreamWriter.flush(); + xmppStreamWriter.close(); + String xml = stanzaWriter.toString(); + logger.info("s2s (out): {}", xml); + sendOut(Jid.of(s.getTo().getDomain()), xml); + } catch (XMLStreamException | JAXBException e1) { + logger.info("jaxb exception", e1); + } + } + + public void sendOut(Jid hostname, String xml) { + boolean haveAnyConn = false; + + ConnectionOut connOut = null; + for (ConnectionOut c : outConnections.keySet()) { + if (c.to != null && c.to.equals(hostname)) { + if (c.streamReady) { + connOut = c; + break; + } else { + haveAnyConn = true; + break; + } + } + } + if (connOut != null) { + connOut.send(xml); + return; + } + + boolean haveCache = false; + for (CacheEntry c : outCache) { + if (c.hostname != null && c.hostname.equals(hostname)) { + c.xml += xml; + c.updated = Instant.now(); + haveCache = true; + break; + } + } + if (!haveCache) { + outCache.add(new CacheEntry(hostname, xml)); + } + + if (!haveAnyConn && !closeFlag.get()) { + try { + createDialbackConnection(hostname.toEscapedString(), null, null); + } catch (Exception e) { + logger.warn("dialback error", e); + } + } + } + + void createDialbackConnection(String to, String checkSID, String dbKey) throws Exception { + ConnectionOut connectionOut = new ConnectionOut(getJid(), Jid.of(to), null, null, checkSID, dbKey); + addConnectionOut(connectionOut, Optional.empty()); + service.submit(() -> { + try { + Socket socket = new Socket(); + socket.connect(DNSQueries.getServerAddress(to)); + connectionOut.setInputStream(socket.getInputStream()); + connectionOut.setOutputStream(socket.getOutputStream()); + addConnectionOut(connectionOut, Optional.of(socket)); + connectionOut.addChildParser(new JuickMessage()); + connectionOut.connect(); + } catch (IOException e) { + userService.getActiveJIDs().stream().filter(j -> Jid.of(j).getDomain().equals(to)) + .forEach(j -> { + userService.setActiveStatusForJID(j, UserService.ActiveStatus.Inactive); + logger.info("{} is inactive now", j); + }); + } + }); + } + + public void startDialback(Jid from, String streamId, String dbKey) throws Exception { + Optional c = getConnectionOut(from, false); + if (c.isPresent()) { + c.get().sendDialbackVerify(streamId, dbKey); + } else { + createDialbackConnection(from.toEscapedString(), streamId, dbKey); + } + } + + public void addStanzaListener(StanzaListener listener) { + stanzaListeners.add(listener); + } + + public void onStanzaReceived(String xmlValue) { + Stanza stanza = parse(xmlValue); + stanzaListeners.forEach(l -> l.stanzaReceived(stanza)); + } + + public BasicXmppSession getSession() { + return session; + } + + public List getInConnections() { + return inConnections; + } + + public Map> getOutConnections() { + return outConnections; + } + + @Override + public void starttls(ConnectionIn connection) { + logger.info("stream {} securing", connection.streamID); + connection.sendStanza(""); + try { + connection.setSocket(sc.getSocketFactory().createSocket(connection.getSocket(), connection.getSocket().getInetAddress().getHostAddress(), + connection.getSocket().getPort(), true)); + ((SSLSocket) connection.getSocket()).setUseClientMode(false); + ((SSLSocket) connection.getSocket()).startHandshake(); + connection.setSecured(true); + logger.info("stream {} secured", connection.streamID); + connection.restartParser(); + } catch (XmlPullParserException | IOException sex) { + logger.warn("stream {} ssl error {}", connection.streamID, sex); + connection.sendStanza(""); + removeConnectionIn(connection); + connection.closeConnection(); + } + } + + @Override + public void proceed(ConnectionOut connection) { + try { + Socket socket = outConnections.get(connection).get(); + socket = sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(), + socket.getPort(), true); + ((SSLSocket) socket).startHandshake(); + connection.setSecured(true); + logger.info("stream {} secured", connection.getStreamID()); + connection.setInputStream(socket.getInputStream()); + connection.setOutputStream(socket.getOutputStream()); + connection.restartStream(); + connection.sendOpenStream(); + } catch (NoSuchElementException | XmlPullParserException | IOException sex) { + logger.error("s2s ssl error: {} {}, error {}", connection.to, connection.getStreamID(), sex); + connection.send(""); + removeConnectionOut(connection); + connection.logoff(); + } + } + + @Override + public void verify(ConnectionOut connection, String from, String type, String sid) { + if (from != null && from.equals(connection.to.toEscapedString()) && sid != null && !sid.isEmpty() && type != null) { + getConnectionIn(sid).ifPresent(c -> c.sendDialbackResult(Jid.of(from), type)); + } + } + + @Override + public void dialbackError(ConnectionOut connection, StreamError error) { + logger.warn("Stream error from {}: {}", connection.getStreamID(), error.getCondition()); + removeConnectionOut(connection); + connection.logoff(); + } + + @Override + public void finished(ConnectionOut connection, boolean dirty) { + logger.warn("stream to {} {} finished, dirty={}", connection.to, connection.getStreamID(), dirty); + removeConnectionOut(connection); + connection.logoff(); + } + + @Override + public void exception(ConnectionOut connection, Exception ex) { + logger.error("s2s out exception: {} {}, exception {}", connection.to, connection.getStreamID(), ex); + removeConnectionOut(connection); + connection.logoff(); + } + + @Override + public void ready(ConnectionOut connection) { + logger.info("stream to {} {} ready", connection.to, connection.getStreamID()); + String cache = getFromCache(connection.to); + if (cache != null) { + logger.info("stream to {} {} sending cache", connection.to, connection.getStreamID()); + connection.send(cache); + } + } + + @Override + public boolean securing(ConnectionOut connection) { + return !Arrays.asList(brokenSSLhosts).contains(connection.to.toEscapedString()); + } + + public Stanza parse(String xml) { + try { + Unmarshaller unmarshaller = session.createUnmarshaller(); + return (Stanza)unmarshaller.unmarshal(new StringReader(xml)); + } catch (JAXBException e) { + logger.error("JAXB exception", e); + } + return null; + } + + public Jid getJid() { + return jid; + } + @PreDestroy + public void preDestroy() { + closeFlag.set(true); + } +} diff --git a/juick-server/src/main/java/com/juick/server/api/Index.java b/juick-server/src/main/java/com/juick/server/api/Index.java index dba8357d..b1d87f67 100644 --- a/juick-server/src/main/java/com/juick/server/api/Index.java +++ b/juick-server/src/main/java/com/juick/server/api/Index.java @@ -19,6 +19,8 @@ package com.juick.server.api; import com.juick.Status; import com.juick.server.WebsocketManager; +import com.juick.server.XMPPServer; +import com.juick.xmpp.helpers.XMPPStatus; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -39,16 +41,26 @@ import java.net.URI; public class Index { @Inject private WebsocketManager wsHandler; + @Inject + private XMPPServer xmpp; @RequestMapping(value = "/", method = RequestMethod.GET, headers = "Connection!=Upgrade") public ResponseEntity description() { URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); } - @RequestMapping(value = "/api/status", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Connection!=Upgrade") public Status status() { return Status.getStatus(String.valueOf(wsHandler.getClients().size())); } + @RequestMapping(method = RequestMethod.GET, value = "/xmpp/status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public XMPPStatus xmppStatus() { + XMPPStatus status = new XMPPStatus(); + if (xmpp != null) { + status.setInbound(xmpp.getInConnections()); + status.setOutbound(xmpp.getOutConnections().keySet()); + } + return status; + } } diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java index 3839248d..21c1238b 100644 --- a/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java +++ b/juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java @@ -22,8 +22,12 @@ import com.juick.server.WebsocketManager; import com.juick.server.component.JuickServerComponent; import com.juick.server.component.JuickServerReconnectManager; import com.juick.service.UserService; +import com.juick.xmpp.helpers.JidConverter; +import com.juick.xmpp.s2s.BasicXmppSession; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.*; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; @@ -36,6 +40,9 @@ import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import org.springframework.web.util.UriComponentsBuilder; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.session.debug.LogbackDebugger; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; @@ -46,6 +53,8 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2; import javax.annotation.Nonnull; import javax.inject.Inject; import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Created by aalexeev on 11/12/16. @@ -120,6 +129,27 @@ public class ApiAppConfiguration extends BaseWebConfiguration implements WebSock container.setMaxBinaryMessageBufferSize(8192); return container; } + @Value("${hostname}") + private String hostname; + + @Bean + public ExecutorService service() { + return Executors.newCachedThreadPool(); + } + @Bean + public BasicXmppSession session() { + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.Message.class)) + .debugger(LogbackDebugger.class) + .build(); + return BasicXmppSession.create(hostname, configuration); + } + @Bean + public static ConversionService conversionService() { + DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); + cs.addConverter(new JidConverter()); + return cs; + } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); diff --git a/juick-server/src/main/java/com/juick/xmpp/extensions/JuickMessage.java b/juick-server/src/main/java/com/juick/xmpp/extensions/JuickMessage.java new file mode 100644 index 00000000..c28eee14 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/extensions/JuickMessage.java @@ -0,0 +1,162 @@ +/* + * Juick + * Copyright (C) 2008-2011, Ugnich Anton + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ +package com.juick.xmpp.extensions; + +import com.juick.Tag; +import com.juick.xmpp.StanzaChild; +import com.juick.xmpp.utils.XmlUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; +/** + * + * @author Ugnich Anton + */ +public class JuickMessage extends com.juick.Message implements StanzaChild { + public final static String XMLNS = "http://juick.com/message"; + public final static String TagName = "juick"; + private SimpleDateFormat df; + public JuickMessage() { + df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + public String getXMLNS() { + return XMLNS; + } + @Override + public JuickMessage parse(XmlPullParser parser) throws XmlPullParserException, IOException, ParseException { + JuickMessage jmsg = new JuickMessage(); + final String sMID = parser.getAttributeValue(null, "mid"); + if (sMID != null) { + jmsg.setMid(Integer.parseInt(sMID)); + } + final String sRID = parser.getAttributeValue(null, "rid"); + if (sRID != null) { + jmsg.setRid(Integer.parseInt(sRID)); + } + final String sReplyTo = parser.getAttributeValue(null, "replyto"); + if (sReplyTo != null) { + jmsg.setReplyto(Integer.parseInt(sReplyTo)); + } + final String sPrivacy = parser.getAttributeValue(null, "privacy"); + if (sPrivacy != null) { + jmsg.setPrivacy(Integer.parseInt(sPrivacy)); + } + final String sFriendsOnly = parser.getAttributeValue(null, "friendsonly"); + if (sFriendsOnly != null) { + jmsg.FriendsOnly = true; + } + final String sReadOnly = parser.getAttributeValue(null, "readonly"); + if (sReadOnly != null) { + jmsg.ReadOnly = true; + } + jmsg.setTimestamp(df.parse(parser.getAttributeValue(null, "ts")).toInstant()); + jmsg.setAttachmentType(parser.getAttributeValue(null, "attach")); + while (parser.next() == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + final String xmlns = parser.getNamespace(); + if (tag.equals("body")) { + jmsg.setText(XmlUtils.getTagText(parser)); + } else if (tag.equals(JuickUser.TagName) && xmlns != null && xmlns.equals(JuickUser.XMLNS)) { + jmsg.setUser(new JuickUser().parse(parser)); + } else if (tag.equals("tag")) { + jmsg.getTags().add(new Tag(XmlUtils.getTagText(parser))); + } else { + XmlUtils.skip(parser); + } + } + return jmsg; + } + @Override + public String toString() { + StringBuilder ret = new StringBuilder("<").append(TagName).append(" xmlns=\"").append(XMLNS).append("\""); + if (getMid() > 0) { + ret.append(" mid=\"").append(getMid()).append("\""); + } + if (getRid() > 0) { + ret.append(" rid=\"").append(getRid()).append("\""); + } + if (getReplyto() > 0) { + ret.append(" replyto=\"").append(getReplyto()).append("\""); + } + ret.append(" privacy=\"").append(getPrivacy()).append("\""); + if (FriendsOnly) { + ret.append(" friendsonly=\"1\""); + } + if (ReadOnly) { + ret.append(" readonly=\"1\""); + } + if (getTimestamp() != null) { + ret.append(" ts=\"").append(df.format(Date.from(getTimestamp()))).append("\""); + } + if (getAttachmentType() != null) { + ret.append(" attach=\"").append(getAttachmentType()).append("\""); + } + ret.append(">"); + if (getUser() != null) { + ret.append(JuickUser.toString(getUser())); + } + if (getText() != null) { + ret.append("").append(StringEscapeUtils.escapeXml10(StringUtils.defaultString(getText()))).append(""); + } + for (Tag Tag : getTags()) { + ret.append("").append(StringEscapeUtils.escapeXml10(Tag.getName())).append(""); + } + ret.append(""); + return ret.toString(); + } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JuickMessage)) { + return false; + } + JuickMessage jmsg = (JuickMessage) obj; + return (this.getMid() == jmsg.getMid() && this.getRid() == jmsg.getRid()); + } + @Override + public int compareTo(Object obj) throws ClassCastException { + if (!(obj instanceof JuickMessage)) { + throw new ClassCastException(); + } + JuickMessage jmsg = (JuickMessage) obj; + if (this.getMid() != jmsg.getMid()) { + if (this.getMid() > jmsg.getMid()) { + return -1; + } else { + return 1; + } + } + if (this.getRid() != jmsg.getRid()) { + if (this.getRid() < jmsg.getRid()) { + return -1; + } else { + return 1; + } + } + return 0; + } +} \ No newline at end of file diff --git a/juick-server/src/main/java/com/juick/xmpp/extensions/JuickUser.java b/juick-server/src/main/java/com/juick/xmpp/extensions/JuickUser.java new file mode 100644 index 00000000..10d2b564 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/extensions/JuickUser.java @@ -0,0 +1,65 @@ +/* + * Juick + * Copyright (C) 2008-2011, Ugnich Anton + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ +package com.juick.xmpp.extensions; +import com.juick.xmpp.StanzaChild; +import com.juick.xmpp.utils.XmlUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +/** + * + * @author Ugnich Anton + */ +public class JuickUser extends com.juick.User implements StanzaChild { + public final static String XMLNS = "http://juick.com/user"; + public final static String TagName = "user"; + public JuickUser() { + } + @Override + public String getXMLNS() { + return XMLNS; + } + @Override + public JuickUser parse(final XmlPullParser parser) throws XmlPullParserException, IOException { + JuickUser juser = new JuickUser(); + String strUID = parser.getAttributeValue(null, "uid"); + if (strUID != null) { + juser.setUid(Integer.parseInt(strUID)); + } + juser.setName(parser.getAttributeValue(null, "uname")); + XmlUtils.skip(parser); + return juser; + } + public static String toString(com.juick.User user) { + String str = "<" + TagName + " xmlns='" + XMLNS + "'"; + if (user.getUid() > 0) { + str += " uid='" + user.getUid() + "'"; + } + if (user.getName() != null && user.getName().length() > 0) { + str += " uname='" + StringEscapeUtils.escapeXml10(user.getName()) + "'"; + } + str += "/>"; + return str; + } + @Override + public String toString() { + return toString(this); + } +} \ No newline at end of file diff --git a/juick-server/src/main/java/com/juick/xmpp/helpers/JidConverter.java b/juick-server/src/main/java/com/juick/xmpp/helpers/JidConverter.java new file mode 100644 index 00000000..253c50f8 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/helpers/JidConverter.java @@ -0,0 +1,13 @@ +package com.juick.xmpp.helpers; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; +import rocks.xmpp.addr.Jid; + +public class JidConverter implements Converter { + @Nullable + @Override + public Jid convert(String jidStr) { + return Jid.of(jidStr); + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/helpers/XMPPStatus.java b/juick-server/src/main/java/com/juick/xmpp/helpers/XMPPStatus.java new file mode 100644 index 00000000..a7fa1bce --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/helpers/XMPPStatus.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.helpers; + +import com.juick.xmpp.s2s.ConnectionIn; +import com.juick.xmpp.s2s.ConnectionOut; + +import java.util.List; +import java.util.Set; + +/** + * Created by vitalyster on 16.02.2017. + */ +public class XMPPStatus { + private List inbound; + private Set outbound; + + public List getInbound() { + return inbound; + } + + public void setInbound(List inbound) { + this.inbound = inbound; + } + + public Set getOutbound() { + return outbound; + } + + public void setOutbound(Set outbound) { + this.outbound = outbound; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/BasicXmppSession.java b/juick-server/src/main/java/com/juick/xmpp/s2s/BasicXmppSession.java new file mode 100644 index 00000000..856d757b --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/BasicXmppSession.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.session.ConnectionConfiguration; +import rocks.xmpp.core.session.XmppSession; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.stanza.model.IQ; +import rocks.xmpp.core.stanza.model.Message; +import rocks.xmpp.core.stanza.model.Presence; +import rocks.xmpp.core.stanza.model.server.ServerIQ; +import rocks.xmpp.core.stanza.model.server.ServerMessage; +import rocks.xmpp.core.stanza.model.server.ServerPresence; +import rocks.xmpp.core.stream.model.StreamElement; + +/** + * Created by vitalyster on 06.02.2017. + */ +public class BasicXmppSession extends XmppSession { + protected BasicXmppSession(String xmppServiceDomain, XmppSessionConfiguration configuration, ConnectionConfiguration... connectionConfigurations) { + super(xmppServiceDomain, configuration, connectionConfigurations); + } + + public static BasicXmppSession create(String xmppServiceDomain, XmppSessionConfiguration configuration) { + BasicXmppSession session = new BasicXmppSession(xmppServiceDomain, configuration); + notifyCreationListeners(session); + return session; + } + + @Override + public void connect(Jid from) throws XmppException { + + } + + @Override + public Jid getConnectedResource() { + return null; + } + + @Override + protected StreamElement prepareElement(StreamElement element) { + if (element instanceof Message) { + element = ServerMessage.from((Message) element); + } else if (element instanceof Presence) { + element = ServerPresence.from((Presence) element); + } else if (element instanceof IQ) { + element = ServerIQ.from((IQ) element); + } + + return element; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/CacheEntry.java b/juick-server/src/main/java/com/juick/xmpp/s2s/CacheEntry.java new file mode 100644 index 00000000..580682cb --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/CacheEntry.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import rocks.xmpp.addr.Jid; + +import java.time.Instant; + +/** + * + * @author ugnich + */ +public class CacheEntry { + + public Jid hostname; + public Instant created; + public Instant updated; + public String xml; + + public CacheEntry(Jid hostname, String xml) { + this.hostname = hostname; + this.created = this.updated =Instant.now(); + this.xml = xml; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/Connection.java b/juick-server/src/main/java/com/juick/xmpp/s2s/Connection.java new file mode 100644 index 00000000..638e3b58 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/Connection.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.juick.server.XMPPServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +/** + * + * @author ugnich + */ +public class Connection { + + protected static final Logger logger = LoggerFactory.getLogger(Connection.class); + + public String streamID; + public Instant created; + public Instant updated; + public long bytesLocal = 0; + public long packetsLocal = 0; + XMPPServer xmpp; + private Socket socket; + public static final String NS_DB = "jabber:server:dialback"; + public static final String NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls"; + public static final String NS_STREAM = "http://etherx.jabber.org/streams"; + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParser parser = factory.newPullParser(); + OutputStreamWriter writer; + private boolean secured = false; + + + + public Connection(XMPPServer xmpp) throws XmlPullParserException { + this.xmpp = xmpp; + created = updated = Instant.now(); + } + + public void logParser() { + if (streamID == null) { + return; + } + String tag = "IN: <" + parser.getName(); + for (int i = 0; i < parser.getAttributeCount(); i++) { + tag += " " + parser.getAttributeName(i) + "=\"" + parser.getAttributeValue(i) + "\""; + } + tag += ">...\n"; + logger.trace(tag); + } + + public void sendStanza(String xml) { + if (streamID != null) { + logger.trace("OUT: {}\n", xml); + } + try { + writer.write(xml); + writer.flush(); + } catch (IOException e) { + logger.error("send stanza failed", e); + } + + updated = Instant.now(); + bytesLocal += xml.length(); + packetsLocal++; + } + + public void closeConnection() { + if (streamID != null) { + logger.info("closing stream {}", streamID); + } + + try { + writer.write(""); + } catch (Exception e) { + } + + try { + writer.close(); + } catch (Exception e) { + } + + try { + socket.close(); + } catch (Exception e) { + } + } + + public boolean isSecured() { + return secured; + } + + public void setSecured(boolean secured) { + this.secured = secured; + } + + public void restartParser() throws XmlPullParserException, IOException { + streamID = UUID.randomUUID().toString(); + parser = factory.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new InputStreamReader(socket.getInputStream())); + writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8); + } + + @JsonIgnore + public Socket getSocket() { + return socket; + } + + public void setSocket(Socket socket) { + this.socket = socket; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionIn.java b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionIn.java new file mode 100644 index 00000000..94da89d2 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionIn.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import com.juick.server.XMPPServer; +import com.juick.xmpp.extensions.StreamError; +import com.juick.xmpp.utils.XmlUtils; +import org.apache.commons.lang3.StringUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import rocks.xmpp.addr.Jid; + +import java.io.EOFException; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author ugnich + */ +public class ConnectionIn extends Connection implements Runnable { + + final public List from = new CopyOnWriteArrayList<>(); + public Instant received; + public long packetsRemote = 0; + ConnectionListener listener; + + public ConnectionIn(XMPPServer xmpp, Socket socket) throws XmlPullParserException, IOException { + super(xmpp); + this.setSocket(socket); + restartParser(); + } + + @Override + public void run() { + try { + parser.next(); // stream:stream + updateTsRemoteData(); + if (!parser.getName().equals("stream") + || !parser.getNamespace("stream").equals(NS_STREAM)) { +// || !parser.getAttributeValue(null, "version").equals("1.0") +// || !parser.getAttributeValue(null, "to").equals(Main.HOSTNAME)) { + throw new Exception(String.format("stream from %s invalid", getSocket().getRemoteSocketAddress())); + } + streamID = parser.getAttributeValue(null, "id"); + if (streamID == null) { + streamID = UUID.randomUUID().toString(); + } + boolean xmppversionnew = parser.getAttributeValue(null, "version") != null; + String from = parser.getAttributeValue(null, "from"); + + if (Arrays.asList(xmpp.bannedHosts).contains(from)) { + closeConnection(); + return; + } + sendOpenStream(from, xmppversionnew); + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + updateTsRemoteData(); + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + logParser(); + + packetsRemote++; + + String tag = parser.getName(); + if (tag.equals("result") && parser.getNamespace().equals(NS_DB)) { + String dfrom = parser.getAttributeValue(null, "from"); + String to = parser.getAttributeValue(null, "to"); + logger.info("stream from {} to {} {} asking for dialback", dfrom, to, streamID); + if (dfrom.endsWith(xmpp.getJid().toEscapedString()) && (dfrom.equals(xmpp.getJid().toEscapedString()) + || dfrom.endsWith("." + xmpp.getJid()))) { + logger.warn("stream from {} is invalid", dfrom); + break; + } + if (to != null && to.equals(xmpp.getJid().toEscapedString())) { + String dbKey = XmlUtils.getTagText(parser); + updateTsRemoteData(); + xmpp.startDialback(Jid.of(dfrom), streamID, dbKey); + } else { + logger.warn("stream from " + dfrom + " " + streamID + " invalid to " + to); + break; + } + } else if (tag.equals("verify") && parser.getNamespace().equals(NS_DB)) { + String vfrom = parser.getAttributeValue(null, "from"); + String vto = parser.getAttributeValue(null, "to"); + String vid = parser.getAttributeValue(null, "id"); + String vkey = XmlUtils.getTagText(parser); + updateTsRemoteData(); + final boolean[] valid = {false}; + if (vfrom != null && vto != null && vid != null && vkey != null) { + xmpp.getConnectionOut(Jid.of(vfrom), false).ifPresent(c -> { + String dialbackKey = c.dbKey; + valid[0] = vkey.equals(dialbackKey); + }); + } + if (valid[0]) { + sendStanza(""); + logger.info("stream from {} {} dialback verify valid", vfrom, streamID); + } else { + sendStanza(""); + logger.warn("stream from {} {} dialback verify invalid", vfrom, streamID); + } + } else if (tag.equals("presence") && checkFromTo(parser)) { + String xml = XmlUtils.parseToString(parser, false); + logger.info("stream {} presence: {}", streamID, xml); + xmpp.onStanzaReceived(xml); + } else if (tag.equals("message") && checkFromTo(parser)) { + updateTsRemoteData(); + String xml = XmlUtils.parseToString(parser, false); + logger.info("stream {} message: {}", streamID, xml); + xmpp.onStanzaReceived(xml); + + } else if (tag.equals("iq") && checkFromTo(parser)) { + updateTsRemoteData(); + String type = parser.getAttributeValue(null, "type"); + String xml = XmlUtils.parseToString(parser, false); + if (type == null || !type.equals("error")) { + logger.info("stream {} iq: {}", streamID, xml); + xmpp.onStanzaReceived(xml); + } + } else if (!isSecured() && tag.equals("starttls")) { + listener.starttls(this); + } else if (isSecured() && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) { + sendOpenStream(null, true); + } else if (tag.equals("error")) { + StreamError streamError = StreamError.parse(parser); + logger.warn("Stream error from {}: {}", streamID, streamError.getCondition()); + xmpp.removeConnectionIn(this); + closeConnection(); + } else { + String unhandledStanza = XmlUtils.parseToString(parser, true); + logger.warn("Unhandled stanza from {}: {}", streamID, unhandledStanza); + } + } + logger.warn("stream {} finished", streamID); + xmpp.removeConnectionIn(this); + closeConnection(); + } catch (EOFException | SocketException ex) { + logger.info("stream {} closed (dirty)", streamID); + xmpp.removeConnectionIn(this); + closeConnection(); + } catch (Exception e) { + logger.warn("stream {} error {}", streamID, e); + xmpp.removeConnectionIn(this); + closeConnection(); + } + } + + void updateTsRemoteData() { + received = Instant.now(); + } + + void sendOpenStream(String from, boolean xmppversionnew) throws IOException { + String openStream = ""; + if (xmppversionnew) { + openStream += ""; + if (listener != null && !isSecured() && !Arrays.asList(xmpp.brokenSSLhosts).contains(from)) { + openStream += ""; + } + openStream += ""; + } + sendStanza(openStream); + } + + public void sendDialbackResult(Jid sfrom, String type) { + sendStanza(""); + if (type.equals("valid")) { + from.add(sfrom); + logger.info("stream from {} {} ready", sfrom, streamID); + } + } + + boolean checkFromTo(XmlPullParser parser) throws Exception { + String cfrom = parser.getAttributeValue(null, "from"); + String cto = parser.getAttributeValue(null, "to"); + if (StringUtils.isNotEmpty(cfrom) && StringUtils.isNotEmpty(cto)) { + Jid jidto = Jid.of(cto); + if (jidto.getDomain().equals(xmpp.getJid().toEscapedString())) { + Jid jidfrom = Jid.of(cfrom); + for (Jid aFrom : from) { + if (aFrom.equals(Jid.of(jidfrom.getDomain()))) { + return true; + } + } + } + } + return false; + } + public void setListener(ConnectionListener listener) { + this.listener = listener; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionListener.java b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionListener.java new file mode 100644 index 00000000..76bc8bdc --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionListener.java @@ -0,0 +1,14 @@ +package com.juick.xmpp.s2s; + +import com.juick.xmpp.extensions.StreamError; + +public interface ConnectionListener { + void starttls(ConnectionIn connection); + void proceed(ConnectionOut connection); + void verify(ConnectionOut connection, String from, String type, String sid); + void dialbackError(ConnectionOut connection, StreamError error); + void finished(ConnectionOut connection, boolean dirty); + void exception(ConnectionOut connection, Exception ex); + void ready(ConnectionOut connection); + boolean securing(ConnectionOut connection); +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionOut.java b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionOut.java new file mode 100644 index 00000000..6c8c9782 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/ConnectionOut.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import com.juick.xmpp.s2s.util.DialbackUtils; +import com.juick.xmpp.Stream; +import com.juick.xmpp.extensions.StreamError; +import com.juick.xmpp.extensions.StreamFeatures; +import com.juick.xmpp.utils.XmlUtils; +import org.apache.commons.text.RandomStringGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmlpull.v1.XmlPullParser; +import rocks.xmpp.addr.Jid; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketException; +import java.util.UUID; + +/** + * @author ugnich + */ +public class ConnectionOut extends Stream { + protected static final Logger logger = LoggerFactory.getLogger(ConnectionOut.class); + public static final String NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls"; + public static final String NS_DB = "jabber:server:dialback"; + private boolean secured = false; + + public boolean streamReady = false; + String checkSID = null; + String dbKey = null; + private String streamID; + ConnectionListener listener; + RandomStringGenerator generator = new RandomStringGenerator.Builder().withinRange('a', 'z').build(); + + public ConnectionOut(Jid from, Jid to, InputStream is, OutputStream os, String checkSID, String dbKey) throws Exception { + super(from, to, is, os); + this.to = to; + this.checkSID = checkSID; + this.dbKey = dbKey; + if (dbKey == null) { + this.dbKey = DialbackUtils.generateDialbackKey(generator.generate(15), to, from, streamID); + } + streamID = UUID.randomUUID().toString(); + } + + public void sendOpenStream() throws IOException { + send(""); + } + + void processDialback() throws Exception { + if (checkSID != null) { + sendDialbackVerify(checkSID, dbKey); + } + send("" + + dbKey + ""); + } + + @Override + public void handshake() { + try { + restartStream(); + + sendOpenStream(); + + parser.next(); // stream:stream + streamID = parser.getAttributeValue(null, "id"); + if (streamID == null || streamID.isEmpty()) { + throw new Exception("stream to " + to + " invalid first packet"); + } + + logger.info("stream to {} {} open", to, streamID); + boolean xmppversionnew = parser.getAttributeValue(null, "version") != null; + if (!xmppversionnew) { + processDialback(); + } + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + String tag = parser.getName(); + if (tag.equals("result") && parser.getNamespace().equals(NS_DB)) { + String type = parser.getAttributeValue(null, "type"); + if (type != null && type.equals("valid")) { + streamReady = true; + listener.ready(this); + } else { + logger.info("stream to {} {} dialback fail", to, streamID); + } + XmlUtils.skip(parser); + } else if (tag.equals("verify") && parser.getNamespace().equals(NS_DB)) { + String from = parser.getAttributeValue(null, "from"); + String type = parser.getAttributeValue(null, "type"); + String sid = parser.getAttributeValue(null, "id"); + listener.verify(this, from, type, sid); + XmlUtils.skip(parser); + } else if (tag.equals("features") && parser.getNamespace().equals(NS_STREAM)) { + StreamFeatures features = StreamFeatures.parse(parser); + if (listener != null && !secured && features.STARTTLS >= 0 + && listener.securing(this)) { + logger.info("stream to {} {} securing", to.toEscapedString(), streamID); + send(""); + } else { + processDialback(); + } + } else if (tag.equals("proceed") && parser.getNamespace().equals(NS_TLS)) { + listener.proceed(this); + } else if (secured && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) { + streamID = parser.getAttributeValue(null, "id"); + } else if (tag.equals("error")) { + StreamError streamError = StreamError.parse(parser); + listener.dialbackError(this, streamError); + } else { + String unhandledStanza = XmlUtils.parseToString(parser, true); + logger.warn("Unhandled stanza from {} {} : {}", to, streamID, unhandledStanza); + } + } + listener.finished(this, false); + } catch (EOFException | SocketException eofex) { + listener.finished(this, true); + } catch (Exception e) { + listener.exception(this, e); + } + } + + public void sendDialbackVerify(String sid, String key) { + send("" + + key + ""); + } + public void setListener(ConnectionListener listener) { + this.listener = listener; + } + + public String getStreamID() { + return streamID; + } + + public boolean isSecured() { + return secured; + } + + public void setSecured(boolean secured) { + this.secured = secured; + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/DNSQueries.java b/juick-server/src/main/java/com/juick/xmpp/s2s/DNSQueries.java new file mode 100644 index 00000000..47b01d7e --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/DNSQueries.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.Hashtable; +import java.util.Random; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +/** + * + * @author ugnich + */ +public class DNSQueries { + + private static final Logger logger = LoggerFactory.getLogger(DNSQueries.class); + + private static Random rand = new Random(); + + public static InetSocketAddress getServerAddress(String hostname) { + + String host = hostname; + int port = 5269; + + Hashtable env = new Hashtable<>(5); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + try { + DirContext ctx = new InitialDirContext(env); + Attribute att = ctx.getAttributes("_xmpp-server._tcp." + hostname, new String[]{"SRV"}).get("SRV"); + + if (att != null && att.size() > 0) { + int i = rand.nextInt(att.size()); + String srv[] = att.get(i).toString().split(" "); + port = NumberUtils.toInt(srv[2], 5269); + host = srv[3]; + } + ctx.close(); + } catch (NamingException e) { + logger.info("SRV record for {} is not resolved, falling back to A record", hostname); + } + return new InetSocketAddress(host, port); + } +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/StanzaListener.java b/juick-server/src/main/java/com/juick/xmpp/s2s/StanzaListener.java new file mode 100644 index 00000000..324a0aa8 --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/StanzaListener.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s; + + +import rocks.xmpp.core.stanza.model.Stanza; + +/** + * Created by vitalyster on 07.12.2016. + */ +public interface StanzaListener { + void stanzaReceived(Stanza xmlValue); +} diff --git a/juick-server/src/main/java/com/juick/xmpp/s2s/util/DialbackUtils.java b/juick-server/src/main/java/com/juick/xmpp/s2s/util/DialbackUtils.java new file mode 100644 index 00000000..a7646deb --- /dev/null +++ b/juick-server/src/main/java/com/juick/xmpp/s2s/util/DialbackUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program 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, or (at your option) any later version. + * + * This program 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 . + */ + +package com.juick.xmpp.s2s.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.HmacUtils; +import rocks.xmpp.addr.Jid; + +/** + * Created by vitalyster on 05.12.2016. + */ +public class DialbackUtils { + private DialbackUtils() { + throw new IllegalStateException(); + } + + public static String generateDialbackKey(String secret, Jid to, Jid from, String id) { + return HmacUtils.hmacSha256Hex(DigestUtils.sha256(secret), + (to.toEscapedString() + " " + from.toEscapedString() + " " + id).getBytes()); + } +} diff --git a/juick-server/src/main/resources/juick.png b/juick-server/src/main/resources/juick.png new file mode 100644 index 00000000..c85ef2c4 Binary files /dev/null and b/juick-server/src/main/resources/juick.png differ diff --git a/juick-server/src/test/java/com/juick/server/tests/XMPPServerTests.java b/juick-server/src/test/java/com/juick/server/tests/XMPPServerTests.java new file mode 100644 index 00000000..5a3c8e69 --- /dev/null +++ b/juick-server/src/test/java/com/juick/server/tests/XMPPServerTests.java @@ -0,0 +1,190 @@ +package com.juick.server.tests; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.juick.Tag; +import com.juick.User; +import com.juick.configuration.RepositoryConfiguration; +import com.juick.server.XMPPBot; +import com.juick.server.XMPPServer; +import com.juick.server.configuration.ApiAppConfiguration; +import com.juick.server.configuration.ApiSecurityConfig; +import com.juick.service.MessagesService; +import com.juick.service.SubscriptionService; +import com.juick.service.TagService; +import com.juick.service.UserService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.Stanza; +import rocks.xmpp.core.stanza.model.server.ServerMessage; + +import javax.inject.Inject; +import java.lang.reflect.InvocationTargetException; +import java.text.ParseException; +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = { + ApiAppConfiguration.class, ApiSecurityConfig.class, RepositoryConfiguration.class +}) +@TestPropertySource(properties = {"broken_ssl_hosts=localhost,serverstorageisfull.tld"}) +public class XMPPServerTests extends AbstractJUnit4SpringContextTests { + @Inject + private WebApplicationContext wac; + @Inject + private XMPPServer server; + @Inject + private XMPPBot bot; + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Inject + private TagService tagService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private JdbcTemplate jdbcTemplate; + @Value("${hostname}") + private Jid jid; + + private MockMvc mockMvc; + + @Before + public void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + } + @Test + public void statusPageIsUp() throws Exception { + mockMvc.perform(get("http://localhost:8080/status")).andExpect(status().isOk()); + assertThat(server.getJid(), equalTo(jid)); + } + @Test + public void botIsUpAndProcessingResourceConstraints() { + int renhaId; + renhaId = userService.createUser("renha", "umnnbt"); + Jid from = Jid.of("renha@serverstorageisfull.tld"); + jdbcTemplate.update("INSERT INTO jids(user_id,jid,active) VALUES(?,?,?)", renhaId, from.toEscapedString(), 1); + String xmlMessage = "Reply by @LexXПохоже нынче можно публично заявлять о своем веганстве. Your contact offline message queue is full. The message has been discarded."; + Stanza msg = server.parse(xmlMessage); + assertThat(from, equalTo(msg.getFrom())); + boolean isActive = jdbcTemplate.queryForObject("SELECT active FROM jids WHERE user_id=?", Integer.class, renhaId) == 1; + assertThat(isActive, equalTo(true)); + bot.incomingMessage((ServerMessage)msg); + isActive = jdbcTemplate.queryForObject("SELECT active FROM jids WHERE user_id=?", Integer.class, renhaId) == 1; + assertThat(isActive, equalTo(false)); + } + @Test + public void botCommandsTests() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { + assertThat(bot.processCommand(new User(), Jid.of("test@localhost"), "PING").get(), is("PONG")); + // subscription commands have two lines, others have 1 + assertThat(bot.processCommand(new User(), Jid.of("test@localhost"), "help").get().split("\n").length, is(23)); + } + + @Test + public void protocolTests() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ParseException, JsonProcessingException { + int uid = userService.createUser("me", "secret"); + User user = userService.getUserByUID(uid).orElse(new User()); + Tag yo = tagService.getTag("yo", true); + int mid = messagesService.createMessage(uid, "yoyo", null, Collections.singletonList(yo)); + assertEquals("should be message", true, + bot.processCommand(user, Jid.of("test@localhost"), String.format("#%d", mid)).get().startsWith("@me")); + mid = messagesService.getUserBlog(user.getUid(), -1, 0).stream().reduce((first, second) -> second).get(); + assertEquals("text should match", "yoyo", + messagesService.getMessage(mid).getText()); + assertEquals("tag should match", "yo", + tagService.getMessageTags(mid).get(0).getTag().getName()); + int readerUid = userService.createUser("dummyReader", "dummySecret"); + User readerUser = userService.getUserByUID(readerUid).orElse(new User()); + assertEquals("should be subscribed", "Subscribed", + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "S #" + mid).get()); + /* TODO: move from juick-legacy + assertEquals("should be favorited", "Message added to your recommendations", + juickProtocol.getReply(readerUser, "! #" + mid)); + */ + assertEquals("number of subscribed users should match", 1, + subscriptionService.getUsersSubscribedToComments(mid, uid).size()); + /* + assertEquals("should be subscribed", "Subscribed", + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "S @" + user.getName()).get()); + List friends = userService.getUserFriends(readerUid); + assertEquals("number of friend users should match", 2, + friends.size()); + assertEquals("number of reader users should match", 1, + userService.getUserReaders(uid).size()); + String expectedReply = "Reply posted.\n#" + mid + "/1 " + + "http://juick.com/" + mid + "#1"; + String expectedSecondReply = "Reply posted.\n#" + mid + "/2 " + + "http://juick.com/" + mid + "#2"; + assertEquals("should be reply", expectedReply, + bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " yoyo").get()); + assertEquals("should be second reply", expectedSecondReply, + bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + "/1 yoyo").get()); + Message reply = messagesService.getReplies(mid).stream().filter(m -> m.getRid() == 2).findFirst() + .orElse(new Message()); + assertEquals("should be reply to first comment", 1, reply.getReplyto()); + assertNotEquals("tags should NOT be updated", "Tags are updated", + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "#" + mid + " *yo *there").get()); + assertEquals("tags should be updated", "Tags are updated", + bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " *there").get()); + assertEquals("number of tags should match", 2, + tagService.getMessageTags(mid).size()); + assertEquals("should be blacklisted", "Tag added to your blacklist", + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "BL *there").get()); + assertEquals("number of subscribed users should match", 0, + subscriptionService.getSubscribedUsers(uid, mid).size()); + assertEquals("tags should be updated", "Tags are updated", + bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " *there").get()); + assertEquals("number of tags should match", 1, + tagService.getMessageTags(mid).size()); + int taggerUid = userService.createUser("dummyTagger", "dummySecret"); + User taggerUser = userService.getUserByUID(taggerUid).orElse(new User()); + assertEquals("should be subscribed", "Subscribed", + bot.processCommand(taggerUser, Jid.of("tagger@localhost"), "S *yo").get()); + assertEquals("number of subscribed users should match", 2, + subscriptionService.getSubscribedUsers(uid, mid).size()); + assertEquals("should be unsubscribed", "Unsubscribed from yo", + bot.processCommand(taggerUser, Jid.of("tagger@localhost"), "U *yo").get()); + assertEquals("number of subscribed users should match", 1, + subscriptionService.getSubscribedUsers(uid, mid).size()); + assertEquals("number of readers should match", 1, + userService.getUserReaders(uid).size()); + String readerFeed = bot.processCommand(readerUser, Jid.of("dummy@localhost"), "#").get(); + assertEquals("description should match", true, readerFeed.startsWith("Your feed")); + assertEquals("should be unsubscribed", "Unsubscribed from @" + user.getName(), + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "U @" + user.getName()).get()); + assertEquals("number of readers should match", 0, + userService.getUserReaders(uid).size()); + assertEquals("number of friends should match", 1, + userService.getUserFriends(uid).size()); + assertEquals("should be unsubscribed", "Unsubscribed from #" + mid, + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "u #" + mid).get()); + assertEquals("number of subscribed users should match", 0, + subscriptionService.getUsersSubscribedToComments(mid, uid).size()); + assertNotEquals("should NOT be deleted", String.format("Message %s deleted", mid), + bot.processCommand(readerUser, Jid.of("dummy@localhost"), "D #" + mid).get()); + assertEquals("should be deleted", String.format("Message %s deleted", mid), + bot.processCommand(user, Jid.of("test@localhost"), "D #" + mid).get()); + assertEquals("should not have messages", 0, messagesService.getAll(user.getUid(), 0).size()); + */ + } +} diff --git a/juick-xmpp-wip/build.gradle b/juick-xmpp-wip/build.gradle index 75870140..65d84755 100644 --- a/juick-xmpp-wip/build.gradle +++ b/juick-xmpp-wip/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'org.akhikhl.gretty' dependencies { compile project(':juick-server-jdbc') compile project(':juick-server-web') - compile project(':juick-xmpp') + compile project(':juick-server') testCompile project(path: ':juick-core', configuration: 'testArtifacts') testCompile project(path: ':juick-server-web', configuration: 'testArtifacts') diff --git a/juick-xmpp-wip/src/main/java/com/juick/components/XMPPRouter.java b/juick-xmpp-wip/src/main/java/com/juick/components/XMPPRouter.java index 47e76cd8..0fad892d 100644 --- a/juick-xmpp-wip/src/main/java/com/juick/components/XMPPRouter.java +++ b/juick-xmpp-wip/src/main/java/com/juick/components/XMPPRouter.java @@ -17,7 +17,7 @@ package com.juick.components; -import com.juick.components.s2s.BasicXmppSession; +import com.juick.xmpp.s2s.BasicXmppSession; import com.juick.xmpp.Message; import com.juick.xmpp.StreamComponentServer; import com.juick.xmpp.StreamListener; diff --git a/juick-xmpp-wip/src/test/java/com/juick/xmpp/XMPPTests.java b/juick-xmpp-wip/src/test/java/com/juick/xmpp/XMPPTests.java index 971e849a..1f7107bf 100644 --- a/juick-xmpp-wip/src/test/java/com/juick/xmpp/XMPPTests.java +++ b/juick-xmpp-wip/src/test/java/com/juick/xmpp/XMPPTests.java @@ -18,11 +18,11 @@ package com.juick.xmpp; import com.gargoylesoftware.htmlunit.WebClient; -import com.juick.components.XMPPConnection; import com.juick.components.XMPPRouter; -import com.juick.components.configuration.XmppAppConfiguration; import com.juick.configuration.MockDataConfiguration; +import com.juick.server.XMPPConnection; import com.juick.server.configuration.BaseWebConfiguration; +import com.juick.server.configuration.XmppAppConfiguration; import com.juick.service.PrivacyQueriesService; import com.juick.service.ShowQueriesService; import org.junit.Before; diff --git a/juick-xmpp/build.gradle b/juick-xmpp/build.gradle deleted file mode 100644 index 6d7ca2e8..00000000 --- a/juick-xmpp/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'java' -apply plugin: 'war' -apply plugin: 'org.akhikhl.gretty' - -dependencies { - compile project(':juick-server-jdbc') - compile project(':juick-server-web') - compile ('com.github.juick:com.juick.xmpp:658f8cf751') { - exclude group: 'xmlpull' - } - providedCompile 'xpp3:xpp3:1.1.4c' - testCompile project(path: ':juick-core', configuration: 'testArtifacts') - testCompile project(path: ':juick-server-web', configuration: 'testArtifacts') - testCompile project(path: ':juick-server-jdbc', configuration: 'testArtifacts') -} - -compileJava.options.encoding = 'UTF-8' - -gretty { - httpPort = 8080 - contextPath = '/s2s' - servletContainer = 'tomcat8' -} - -configurations { - all*.exclude module: 'commons-logging' - all*.exclude module: 'slf4j-nop' -} - diff --git a/juick-xmpp/src/main/java/com/juick/components/CleaningUp.java b/juick-xmpp/src/main/java/com/juick/components/CleaningUp.java deleted file mode 100644 index a96b7c35..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/CleaningUp.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import javax.inject.Inject; -import java.time.Duration; -import java.time.Instant; - -/** - * - * @author ugnich - */ -@Component -public class CleaningUp { - - private static final Logger logger = LoggerFactory.getLogger(CleaningUp.class); - - private static final int TIMEOUT_MINUTES = 15; - - @Inject - XMPPServer xmpp; - - @Scheduled(fixedDelay = 10000) - public void cleanUp() { - Instant now = Instant.now(); - xmpp.getOutConnections().keySet().stream().filter(c -> Duration.between(now, c.getUpdated()).toMinutes() > TIMEOUT_MINUTES) - .forEach(c -> { - logger.info("closing idle outgoing connection to {}", c.to); - c.logoff(); - xmpp.getOutConnections().remove(c); - }); - - xmpp.getInConnections().stream().filter(c -> Duration.between(now, c.updated).toMinutes() > TIMEOUT_MINUTES) - .forEach(c -> { - logger.info("closing idle incoming connection from {}", c.from); - c.closeConnection(); - xmpp.getInConnections().remove(c); - }); - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/JuickBot.java b/juick-xmpp/src/main/java/com/juick/components/JuickBot.java deleted file mode 100644 index ad21b577..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/JuickBot.java +++ /dev/null @@ -1,710 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components; - -import com.juick.Tag; -import com.juick.User; -import com.juick.components.s2s.StanzaListener; -import com.juick.formatters.PlainTextFormatter; -import com.juick.server.helpers.TagStats; -import com.juick.server.protocol.annotation.UserCommand; -import com.juick.service.*; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.apache.commons.lang3.reflect.MethodUtils; -import org.ocpsoft.prettytime.PrettyTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.*; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.client.ClientPresence; -import rocks.xmpp.core.stanza.model.errors.Condition; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * - * @author ugnich - */ -@Component -public class JuickBot implements StanzaListener, AutoCloseable { - - private static final Logger logger = LoggerFactory.getLogger(JuickBot.class); - - @Inject - private XMPPServer xmpp; - @Inject - private XMPPConnection router; - @Value("${xmppbot_jid}") - private Jid jid; - - private PrettyTime pt; - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private TagService tagService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private ShowQueriesService showQueriesService; - @Inject - private PrivacyQueriesService privacyQueriesService; - @Inject - private SubscriptionService subscriptionService; - - @PostConstruct - public void init() { - xmpp.addStanzaListener(this); - broadcastPresence(null); - pt = new PrettyTime(new Locale("ru")); - } - - public Jid getJid() { - return jid; - } - - public boolean incomingPresence(Presence p) { - final String username = p.getTo().getLocal(); - final boolean toJuick = username.equals(jid.getLocal()); - - if (p.getType() == null) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().asBareJid()); - reply.setTo(p.getFrom().asBareJid()); - reply.setType(Presence.Type.UNSUBSCRIBE); - xmpp.sendOut(ClientPresence.from(reply)); - return true; - } else if (p.getType().equals(Presence.Type.PROBE)) { - int uid_to = 0; - if (!toJuick) { - uid_to = userService.getUIDbyName(username); - } - - if (toJuick || uid_to > 0) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().withResource(jid.getResource())); - reply.setTo(p.getFrom()); - reply.setPriority((byte)10); - if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { - reply.setStatus("Send ON to enable notifications"); - } - xmpp.sendOut(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - xmpp.sendOut(ClientPresence.from(reply)); - return true; - } - return true; - } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { - boolean canSubscribe = false; - if (toJuick) { - canSubscribe = true; - } else { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - canSubscribe = true; - } - } - - if (canSubscribe) { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.SUBSCRIBED); - xmpp.sendOut(ClientPresence.from(reply)); - - reply.setFrom(reply.getFrom().withResource(jid.getResource())); - reply.setPriority((byte) 10); - reply.setType(null); - xmpp.sendOut(ClientPresence.from(reply)); - - return true; - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - xmpp.sendOut(ClientPresence.from(reply)); - return true; - } - } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { - if (!toJuick) { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - } - } - - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.UNSUBSCRIBED); - xmpp.sendOut(ClientPresence.from(reply)); - } - - return false; - } - - public boolean incomingMessage(Message msg) { - if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { - StanzaError error = msg.getError(); - if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { - // offline query is full, deactivating this jid - if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { - logger.info("{} is inactive now", msg.getFrom()); - return true; - } - } - return false; - } - if (StringUtils.isBlank(msg.getBody())) { - return false; - } - String username = msg.getTo().getLocal(); - - User user_from; - String signuphash = StringUtils.EMPTY; - user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); - if (user_from == null) { - signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); - } - - if (user_from == null) { - Message reply = new Message(); - reply.setFrom(msg.getTo()); - reply.setTo(msg.getFrom()); - reply.setType(Message.Type.CHAT); - if (username.equals(jid.getLocal())) { - reply.setBody("Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); - } else { - reply.setBody("Внимание, системное сообщение!\nВаш JabberID не обнаружен в списке доверенных. Для того, чтобы отправить сообщение пользователю " + username + "@juick.com, пожалуйста зарегистрируйте свой JabberID в системе: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nWarning, system message!\nYour JabberID is not found in our server's white list. To send a message to " + username + "@juick.com, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); - } - xmpp.sendOut(ClientMessage.from(reply)); - return true; - } - - if (username.equals(jid.getLocal())) { - try { - return incomingMessageJuick(user_from, msg); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - return false; - } - } - - int uid_to = userService.getUIDbyName(username); - - if (uid_to == 0) { - Message reply = new Message(); - reply.setFrom(msg.getTo()); - reply.setTo(msg.getFrom()); - reply.setType(Message.Type.ERROR); - reply.setId(msg.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - xmpp.sendOut(ClientMessage.from(reply)); - return true; - } - - boolean success = false; - if (!userService.isInBLAny(uid_to, user_from.getUid())) { - success = pmQueriesService.createPM(user_from.getUid(), uid_to, msg.getBody()); - } - - if (success) { - Message m = new Message(); - m.setFrom(jid.asBareJid()); - m.setTo(Jid.of(Integer.toString(uid_to), "push.juick.com", null)); - com.juick.Message jmsg = new com.juick.Message(); - jmsg.setUser(user_from); - jmsg.setText(msg.getBody()); - m.addExtension(jmsg); - router.sendStanza(m); - - m.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); - router.sendStanza(m); - - List jids; - boolean inroster = false; - jids = userService.getJIDsbyUID(uid_to); - for (String userJid : jids) { - Message mm = new Message(); - mm.setTo(Jid.of(userJid)); - mm.setType(Message.Type.CHAT); - inroster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); - if (inroster) { - mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); - mm.setBody(msg.getBody()); - } else { - mm.setFrom(jid); - mm.setBody("Private message from @" + jmsg.getUser().getName() + ":\n" + msg.getBody()); - } - xmpp.sendOut(ClientMessage.from(mm)); - } - } else { - Message reply = new Message(); - reply.setFrom(msg.getTo()); - reply.setTo(msg.getFrom()); - reply.setType(Message.Type.ERROR); - reply.setId(msg.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.NOT_ALLOWED)); - xmpp.sendOut(ClientMessage.from(reply)); - } - - return false; - } - - public Optional processCommand(User user, Jid from, String input) throws InvocationTargetException, - IllegalAccessException, NoSuchMethodException { - Optional cmd = MethodUtils.getMethodsListWithAnnotation(getClass(), UserCommand.class).stream() - .filter(m -> Pattern.compile(m.getAnnotation(UserCommand.class).pattern(), - m.getAnnotation(UserCommand.class).patternFlags()).matcher(input).matches()) - .findFirst(); - if (cmd.isPresent()) { - Matcher matcher = Pattern.compile(cmd.get().getAnnotation(UserCommand.class).pattern(), - cmd.get().getAnnotation(UserCommand.class).patternFlags()).matcher(input); - List groups = new ArrayList<>(); - while (matcher.find()) { - for (int i = 1; i <= matcher.groupCount(); i++) { - groups.add(matcher.group(i)); - } - } - return Optional.of((String) getClass().getMethod(cmd.get().getName(), User.class, Jid.class, String[].class) - .invoke(this, user, from, groups.toArray(new String[groups.size()]))); - } - return Optional.empty(); - } - public boolean incomingMessageJuick(User user_from, Message msg) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { - String command = msg.getBody().trim(); - int commandlen = command.length(); - - // COMPATIBILITY - if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { - command = command.substring(3).trim(); - } - - Optional result = processCommand(user_from, msg.getFrom(), command); - result.ifPresent(r -> sendReply(msg.getFrom(), r)); - return result.isPresent(); - } - - @UserCommand(pattern = "^ping$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "PING - returns you a PONG") - public String commandPing(User user, Jid from, String[] input) { - Presence p = new Presence(from); - p.setFrom(jid); - p.setPriority((byte) 10); - xmpp.sendOut(ClientPresence.from(p)); - return "PONG"; - } - - @UserCommand(pattern = "^help$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "HELP - returns this help message") - public String commandHelp(User user, Jid from, String[] input) { - return Arrays.stream(getClass().getDeclaredMethods()) - .filter(m -> m.isAnnotationPresent(UserCommand.class)) - .map(m -> m.getAnnotation(UserCommand.class).help()) - .collect(Collectors.joining("\n")); - } - - @UserCommand(pattern = "^login$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "LOGIN - log in to Juick website") - public String commandLogin(User user_from, Jid from, String[] input) { - return "http://juick.com/login?hash=" + userService.getHashByUID(user_from.getUid()); - } - @UserCommand(pattern = "^\\@(\\S+)\\s+([\\s\\S]+)$", help = "@username message - send PM to username") - public String commandPM(User user_from, Jid from, String... arguments) { - String body = arguments[1]; - int ret = 0; - - User user_to = userService.getUserByName(arguments[0]); - List jids_to = null; - boolean haveInRoster = false; - - if (user_to.getUid() > 0) { - if (!userService.isInBLAny(user_to.getUid(), user_from.getUid())) { - if (pmQueriesService.createPM(user_from.getUid(), user_to.getUid(), body)) { - jids_to = userService.getJIDsbyUID(user_to.getUid()); - ret = 200; - } else { - ret = 500; - } - } else { - ret = 403; - } - } else { - ret = 404; - } - - if (ret == 200) { - Message msg = new Message(); - msg.setFrom(jid.asBareJid()); - msg.setTo(Jid.of(Integer.toString(user_to.getUid()), "push.juick.com", null)); - com.juick.Message jmsg = new com.juick.Message(); - jmsg.setUser(user_from); - jmsg.setTo(user_to); - jmsg.setText(body); - msg.addExtension(jmsg); - router.sendStanza(msg); - - msg.setTo(Jid.of(Integer.toString(user_to.getUid()), "ws.juick.com", null)); - router.sendStanza(msg); - - for (String userJid : jids_to) { - Message mm = new Message(); - mm.setTo(Jid.of(userJid)); - mm.setType(Message.Type.CHAT); - haveInRoster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); - if (haveInRoster) { - mm.setFrom(Jid.of(user_from.getName(), "juick.com", "Juick")); - mm.setBody(body); - } else { - mm.setFrom(jid); - mm.setBody("Private message from @" + user_from.getName() + ":\n" + body); - } - xmpp.sendOut(ClientMessage.from(mm)); - } - } - - if (ret == 200) { - return "Private message sent"; - } else { - return "Error " + ret; - } - } - @UserCommand(pattern = "^bl$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "BL - Show your blacklist") - public String commandBLShow(User user_from, Jid from, String... arguments) { - List blusers = userService.getUserBLUsers(user_from.getUid()); - List bltags = tagService.getUserBLTags(user_from.getUid()); - - String txt = StringUtils.EMPTY; - if (bltags.size() > 0) { - for (String bltag : bltags) { - txt += "*" + bltag + "\n"; - } - - if (blusers.size() > 0) { - txt += "\n"; - } - } - if (blusers.size() > 0) { - for (User bluser : blusers) { - txt += "@" + bluser.getName() + "\n"; - } - } - if (txt.isEmpty()) { - txt = "You don't have any users or tags in your blacklist."; - } - - return txt; - } - - @UserCommand(pattern = "^#\\+$", help = "#+ - Show last Juick messages") - public String commandLast(User user_from, Jid from, String... arguments) { - return "Last messages:\n" - + printMessages(messagesService.getAll(user_from.getUid(), 0), true); - } - - @UserCommand(pattern = "@", help = "@ - Show recommendations and popular personal blogs") - public String commandUsers(User user_from, Jid from, String... arguments) { - StringBuilder msg = new StringBuilder(); - msg.append("Recommended blogs"); - List recommendedUsers = showQueriesService.getRecommendedUsers(user_from); - if (recommendedUsers.size() > 0) { - for (String user : recommendedUsers) { - msg.append("\n@").append(user); - } - } else { - msg.append("\nNo recommendations now. Subscribe to more blogs. ;)"); - } - msg.append("\n\nTop 10 personal blogs:"); - List topUsers = showQueriesService.getTopUsers(); - if (topUsers.size() > 0) { - for (String user : topUsers) { - msg.append("\n@").append(user); - } - } else { - msg.append("\nNo top users. Empty DB? ;)"); - } - return msg.toString(); - } - @UserCommand(pattern = "^bl\\s+@([^\\s\\n\\+]+)", patternFlags = Pattern.CASE_INSENSITIVE, - help = "BL @username - add @username to your blacklist") - public String blacklistUser(User user_from, Jid from, String... arguments) { - User blUser = userService.getUserByName(arguments[0]); - if (blUser != null) { - PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistUser(user_from, blUser); - if (result == PrivacyQueriesService.PrivacyResult.Added) { - return "User added to your blacklist"; - } else { - return "User removed from your blacklist"; - } - } - return "User not found"; - } - @UserCommand(pattern = "^bl\\s\\*(\\S+)$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "BL *tag - add *tag to your blacklist") - public String blacklistTag(User user_from, Jid from, String... arguments) { - User blUser = userService.getUserByName(arguments[0]); - if (blUser != null) { - Tag tag = tagService.getTag(arguments[0], false); - if (tag != null) { - PrivacyQueriesService.PrivacyResult result = privacyQueriesService.blacklistTag(user_from, tag); - if (result == PrivacyQueriesService.PrivacyResult.Added) { - return "Tag added to your blacklist"; - } else { - return "Tag removed from your blacklist"; - } - } - } - return "Tag not found"; - } - @UserCommand(pattern = "\\*", help = "* - Show your tags") - public String commandTags(User currentUser, Jid from, String... args) { - List tags = tagService.getUserTagStats(currentUser.getUid()); - String msg = "Your tags: (tag - messages)\n" + - tags.stream() - .map(t -> String.format("\n*%s - %d", t.getTag().getName(), t.getUsageCount())).collect(Collectors.joining()); - return msg; - } - @UserCommand(pattern = "S", help = "S - Show your subscriptions") - public String commandSubscriptions(User currentUser, Jid from, String... args) { - List friends = userService.getUserFriends(currentUser.getUid()); - List tags = subscriptionService.getSubscribedTags(currentUser); - String msg = friends.size() > 0 ? "You are subscribed to users:" + friends.stream().map(u -> "\n@" + u.getName()) - .collect(Collectors.joining()) - : "You are not subscribed to any user."; - msg += tags.size() > 0 ? "\nYou are subscribed to tags:" + tags.stream().map(t -> "\n*" + t) - .collect(Collectors.joining()) - : "\nYou are not subscribed to any tag."; - return msg; - } - @UserCommand(pattern = "!", help = "! - Show your favorite messages") - public String commandFavorites(User currentUser, Jid from, String... args) { - List mids = messagesService.getUserRecommendations(currentUser.getUid(), 0); - if (mids.size() > 0) { - return "Favorite messages: \n" + printMessages(mids, false); - } - return "No favorite messages, try to \"like\" something ;)"; - } - // TODO: target notification - @UserCommand(pattern = "^(s|u)\\s+\\@(\\S+)$", help = "S @username - subscribe to user" + - "\nU @username - unsubscribe from user", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandSubscribeUser(User user, Jid from, String... args) { - boolean subscribe = args[0].equalsIgnoreCase("s"); - User toUser = userService.getUserByName(args[1]); - if (subscribe) { - if (subscriptionService.subscribeUser(user, toUser)) { - return "Subscribed to @" + toUser.getName(); - } - } else { - if (subscriptionService.unSubscribeUser(user, toUser)) { - return "Unsubscribed from @" + toUser.getName(); - } - return "You was not subscribed to @" + toUser.getName(); - } - return "Error"; - } - @UserCommand(pattern = "^(s|u)\\s+\\*(\\S+)$", help = "S *tag - subscribe to tag" + - "\nU *tag - unsubscribe from tag", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandSubscribeTag(User user, Jid from, String... args) { - boolean subscribe = args[0].equalsIgnoreCase("s"); - Tag tag = tagService.getTag(args[1], true); - if (subscribe) { - if (subscriptionService.subscribeTag(user, tag)) { - return "Subscribed"; - } - } else { - if (subscriptionService.unSubscribeTag(user, tag)) { - return "Unsubscribed from " + tag.getName(); - } - return "You was not subscribed to " + tag.getName(); - } - return "Error"; - } - @UserCommand(pattern = "^(s|u)\\s+#(\\d+)$", help = "S #1234 - subscribe to comments" + - "\nU #1234 - unsubscribe from comments", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandSubscribeMessage(User user, Jid from, String... args) { - boolean subscribe = args[0].equalsIgnoreCase("s"); - int mid = NumberUtils.toInt(args[1], 0); - if (messagesService.getMessage(mid) != null) { - if (subscribe) { - if (subscriptionService.subscribeMessage(mid, user.getUid())) { - return "Subscribed"; - } - } else { - if (subscriptionService.unSubscribeMessage(mid, user.getUid())) { - return "Unsubscribed from #" + mid; - } - return "You was not subscribed to #" + mid; - } - } - return "Error"; - } - @UserCommand(pattern = "^(on|off)$", patternFlags = Pattern.CASE_INSENSITIVE, - help = "ON/OFF - Enable/disable subscriptions delivery") - public String commandOnOff(User user, Jid from, String[] input) { - UserService.ActiveStatus newStatus; - String retValUpdated; - if (input[0].toLowerCase().equals("on")) { - newStatus = UserService.ActiveStatus.Active; - retValUpdated = "Notifications are activated for " + from.asBareJid().toEscapedString(); - } else { - newStatus = UserService.ActiveStatus.Inactive; - retValUpdated = "Notifications are disabled for " + from.asBareJid().toEscapedString(); - } - - if (userService.setActiveStatusForJID(from.asBareJid().toEscapedString(), newStatus)) { - return retValUpdated; - } else { - return String.format("Subscriptions status for %s was not changed", from.toEscapedString()); - } - } - @UserCommand(pattern = "^\\@([^\\s\\n\\+]+)(\\+?)$", - help = "@username+ - Show user's info and last 20 messages") - public String commandUser(User user, Jid from, String... arguments) { - User blogUser = userService.getUserByName(arguments[0]); - int page = arguments[1].length(); - if (blogUser.getUid() > 0) { - List mids = messagesService.getUserBlog(blogUser.getUid(), 0, 0); - return String.format("Last messages from @%s:\n%s", arguments[0], - printMessages(mids, false)); - } - return "User not found"; - } - @UserCommand(pattern = "^#(\\d+)(\\+?)$", help = "#1234 - Show message (#1234+ - message with replies)") - public String commandShow(User user, Jid from, String... arguments) { - boolean showReplies = arguments[1].length() > 0; - int mid = NumberUtils.toInt(arguments[0], 0); - if (mid == 0) { - return "Error"; - } - com.juick.Message msg = messagesService.getMessage(mid); - if (msg != null) { - if (showReplies) { - List replies = messagesService.getReplies(mid); - replies.add(0, msg); - return String.join("\n", - replies.stream().map(PlainTextFormatter::formatPostSummary).collect(Collectors.toList())); - } - return PlainTextFormatter.formatPostSummary(msg); - } - return "Message not found"; - } - @UserCommand(pattern = "^D #(\\d+)$", help = "D #1234 - Delete post", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandDeletePost(User user, Jid from, String... args) { - int mid = Integer.valueOf(args[0]); - if (messagesService.deleteMessage(user.getUid(), mid)) { - return "Message deleted"; - } - return "This is not your message"; - } - @UserCommand(pattern = "^D #(\\d+)(\\.|\\-|\\/)(\\d+)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandDeleteReply(User user, Jid from, String... args) { - int mid = Integer.valueOf(args[0]); - int rid = Integer.valueOf(args[2]); - if (messagesService.deleteReply(user.getUid(), mid, rid)) { - return "Reply deleted"; - } else { - return "This is not your reply"; - } - } - /* - @UserCommand(pattern = "(^(D L|DL|D LAST)$", help = "D #1234/5 - Delete comment", patternFlags = Pattern.CASE_INSENSITIVE) - public String commandDeleteLast(User user, Jid from, String... args) { - - } - */ - void sendReply(Jid jidTo, String txt) { - Message reply = new Message(); - reply.setFrom(jid); - reply.setTo(jidTo); - reply.setType(Message.Type.CHAT); - reply.setBody(txt); - xmpp.sendOut(ClientMessage.from(reply)); - } - - void sendNotification(Stanza stanza) { - xmpp.sendOut(stanza); - } - - @Override - public void stanzaReceived(Stanza xmlValue) { - if (xmlValue instanceof Presence) { - Presence p = (Presence) xmlValue; - if (p.getType() == null || !p.getType().equals(Presence.Type.ERROR)) { - incomingPresence(p); - } - } else if (xmlValue instanceof Message) { - Message msg = (Message) xmlValue; - if (!incomingMessage(msg)) { - router.sendStanza(msg); - } - } else if (xmlValue instanceof IQ) { - IQ iq = (IQ) xmlValue; - router.sendStanza(iq); - } - } - - String printMessages(List mids, boolean crop) { - return messagesService.getMessages(mids).stream() - .sorted(Collections.reverseOrder()) - .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); - } - - void broadcastPresence(Presence.Type type) { - Presence presence = new Presence(); - presence.setFrom(jid); - if (type != null) { - presence.setType(type); - } - userService.getActiveJIDs().forEach(j -> { - try { - presence.setTo(Jid.of(j)); - xmpp.sendOut(ClientPresence.from(presence)); - } catch (IllegalArgumentException ex) { - logger.warn("Invalid jid: {}", j, ex); - } - }); - } - - @Override - public void close() throws Exception { - broadcastPresence(Presence.Type.UNAVAILABLE); - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/XMPPConnection.java b/juick-xmpp/src/main/java/com/juick/components/XMPPConnection.java deleted file mode 100644 index 6bca282c..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/XMPPConnection.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components; - -import com.juick.Attachment; -import com.juick.User; -import com.juick.components.s2s.BasicXmppSession; -import com.juick.server.helpers.UserInfo; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.UserService; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.DependsOn; -import org.springframework.stereotype.Component; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.stanza.AbstractIQHandler; -import rocks.xmpp.core.stanza.model.IQ; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.core.stanza.model.Stanza; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.errors.Condition; -import rocks.xmpp.extensions.caps.model.EntityCapabilities; -import rocks.xmpp.extensions.component.accept.ExternalComponent; -import rocks.xmpp.extensions.filetransfer.FileTransfer; -import rocks.xmpp.extensions.filetransfer.FileTransferManager; -import rocks.xmpp.extensions.nick.model.Nickname; -import rocks.xmpp.extensions.oob.model.x.OobX; -import rocks.xmpp.extensions.ping.PingManager; -import rocks.xmpp.extensions.vcard.temp.model.VCard; -import rocks.xmpp.extensions.version.SoftwareVersionManager; -import rocks.xmpp.extensions.version.model.SoftwareVersion; -import rocks.xmpp.util.XmppUtils; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import javax.xml.bind.JAXBException; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import java.io.IOException; -import java.io.StringWriter; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -/** - * @author ugnich - */ -@Component -public class XMPPConnection implements AutoCloseable { - - private static final Logger logger = LoggerFactory.getLogger(XMPPConnection.class); - - private ExternalComponent router; - @Inject - private JuickBot bot; - - @Value("${componentname:localhost}") - private String componentName; - @Value("${component_port:5347}") - private int componentPort; - @Value("${xmpp_password:secret}") - private String password; - @Value("${upload_tmp_dir:/tmp}") - private String tmpDir; - - @Inject - public MessagesService messagesService; - @Inject - public UserService userService; - @Inject - public SubscriptionService subscriptionService; - @Inject - private BasicXmppSession session; - @Inject - private ExecutorService service; - - @PostConstruct - public void init() { - logger.info("stream router start connecting to {}", componentPort); - router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort); - PingManager pingManager = router.getManager(PingManager.class); - pingManager.setEnabled(true); - router.disableFeature(EntityCapabilities.NAMESPACE); - SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); - softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", System.getProperty("os.name", "generic"))); - VCard vCard = new VCard(); - vCard.setFormattedName("Juick"); - try { - vCard.setUrl(new URL("http://juick.com/")); - vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray(getClass().getClassLoader().getResource("juick.png")))); - } catch (MalformedURLException e) { - logger.error("invalid url", e); - } catch (IOException e) { - logger.warn("invalid resource", e); - } - router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { - @Override - protected IQ processRequest(IQ iq) { - if (iq.getTo().equals(bot.getJid()) || iq.getTo().asBareJid().equals(bot.getJid().asBareJid()) - || iq.getTo().asBareJid().toEscapedString().equals(bot.getJid().getDomain())) { - return iq.createResult(vCard); - } - User user = userService.getUserByName(iq.getTo().getLocal()); - if (user.getUid() > 0) { - UserInfo info = userService.getUserInfo(user); - VCard userVCard = new VCard(); - userVCard.setFormattedName(info.getFullName()); - userVCard.setNickname(user.getName()); - try { - userVCard.setPhoto(new VCard.Image(new URI("http://i.juick.com/a/" + user.getUid() + ".png"))); - if (info.getUrl() != null) { - userVCard.setUrl(new URL(info.getUrl())); - } - } catch (MalformedURLException | URISyntaxException e) { - logger.warn("url exception", e); - } - return iq.createResult(userVCard); - } - return iq.createError(Condition.BAD_REQUEST); - } - }); - router.addInboundMessageListener(e -> { - Message message = e.getMessage(); - Jid jid = message.getTo(); - if (jid.getDomain().equals(router.getDomain().toEscapedString())) { - com.juick.Message jmsg = message.getExtension(com.juick.Message.class); - if (jmsg != null) { - if (jid.getLocal().equals("recomm")) { - sendJuickRecommendation(jmsg); - } else { - if (jmsg.getRid() > 0) { - sendJuickComment(jmsg); - } else if (jmsg.getMid() > 0) { - sendJuickMessage(jmsg); - } - } - } - } else if (jid.getDomain().endsWith(bot.getJid().getDomain()) && (jid.getDomain().equals(bot.getJid().getDomain()) - || jid.getDomain().endsWith("." + bot.getJid().getDomain()))) { - if (logger.isInfoEnabled()) { - try { - logger.info("unhandled message: {}", stanzaToString(message)); - } catch (JAXBException | XMLStreamException ex) { - logger.error("JAXB exception", ex); - } - } - } else { - route(ClientMessage.from(message)); - } - }); - router.addInboundIQListener(e -> { - IQ iq = e.getIQ(); - Jid jid = iq.getTo(); - if (!jid.getDomain().equals(bot.getJid().getDomain())) { - route(iq); - } - }); - FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); - fileTransferManager.addFileTransferOfferListener(e -> { - try { - List allowedTypes = new ArrayList() {{ - add("png"); - add("jpg"); - }}; - String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); - String targetFilename = String.format("%s.%s", - DigestUtils.md5Hex(String.format("%s-%s", - e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); - if (allowedTypes.contains(attachmentExtension)) { - Path filePath = Paths.get(tmpDir, targetFilename); - FileTransfer ft = e.accept(filePath).get(); - ft.addFileTransferStatusListener(st -> { - logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); - if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { - logger.info("transfer completed"); - Message msg = new Message(); - msg.setType(Message.Type.CHAT); - msg.setFrom(e.getInitiator()); - msg.setTo(bot.getJid()); - msg.setBody(e.getDescription()); - try { - String attachmentUrl = String.format("juick://%s", targetFilename); - msg.addExtension(new OobX(new URI(attachmentUrl), "!!!!Juick!!")); - router.sendMessage(msg); - } catch (URISyntaxException e1) { - logger.warn("attachment error", e1); - } - } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { - logger.info("transfer failed", ft.getException()); - Message msg = new Message(); - msg.setType(Message.Type.CHAT); - msg.setFrom(bot.getJid()); - msg.setTo(e.getInitiator()); - msg.setBody("File transfer failed, please report to us"); - router.sendMessage(msg); - } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { - logger.info("transfer cancelled"); - } - }); - ft.transfer(); - logger.info("transfer started"); - } else { - e.reject(); - logger.info("transfer rejected"); - } - } catch (IOException | InterruptedException | ExecutionException e1) { - logger.error("ft error", e1); - } - }); - router.addConnectionListener(event -> { - if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { - logger.info("component connected"); - } - }); - service.submit(() -> { - try { - Thread.sleep(3000); - router.connect(); - } catch (InterruptedException | XmppException e) { - logger.warn("xmpp exception", e); - } - }); - } - - String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { - StringWriter stanzaWriter = new StringWriter(); - XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( - router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); - router.createMarshaller().marshal(stanza, xmppStreamWriter); - xmppStreamWriter.flush(); - xmppStreamWriter.close(); - return stanzaWriter.toString(); - } - - void route(Stanza stanza) { - try { - String xml = stanzaToString(stanza); - logger.info("stream router (out): {}", xml); - bot.sendNotification(stanza); - } catch (XMLStreamException | JAXBException e) { - logger.error("JAXB exception", e); - } - - } - - public void sendStanza(Stanza xml) { - router.send(xml); - } - - - - public void sendJuickMessage(com.juick.Message jmsg) { - List jids = new ArrayList<>(); - - if (jmsg.FriendsOnly) { - jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly); - } else { - List users = subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid()); - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - jids.add(jid); - } - } - } - com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()); - String txt = "@" + jmsg.getUser().getName() + ":" + fullMsg.getTagsString() + "\n"; - Attachment attachment = fullMsg.getAttachment(); - if (attachment != null) { - txt += attachment.getMedium().getUrl() + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid() + " http://juick.com/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(bot.getJid()); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (attachment != null) { - try { - OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - for (String jid : jids) { - msg.setTo(Jid.of(jid)); - route(ClientMessage.from(msg)); - } - } - - public void sendJuickComment(com.juick.Message jmsg) { - List users; - String replyQuote; - String replyTo; - - users = subscriptionService.getUsersSubscribedToComments(jmsg.getMid(), jmsg.getUser().getUid()); - com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) - : messagesService.getMessage(jmsg.getMid()); - replyTo = replyMessage.getUser().getName(); - com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - replyQuote = fullReply.getReplyQuote(); - - String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; - Attachment attachment = jmsg.getAttachment(); - if (attachment != null) { - txt += attachment.getMedium().getUrl() + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/" + jmsg.getMid() + "#" + jmsg.getRid(); - - Message msg = new Message(); - msg.setFrom(bot.getJid()); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.addExtension(jmsg); - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - route(ClientMessage.from(msg)); - } - } - } - - public void sendJuickRecommendation(com.juick.Message recomm) { - List users; - com.juick.Message jmsg = messagesService.getMessage(recomm.getMid()); - users = subscriptionService.getUsersSubscribedToUserRecommendations(recomm.getUser().getUid(), - recomm.getMid(), jmsg.getUser().getUid()); - - String txt = "Recommended by @" + recomm.getUser().getName() + ":\n"; - txt += "@" + jmsg.getUser().getName() + ":" + jmsg.getTagsString() + "\n"; - Attachment attachment = jmsg.getAttachment(); - if (attachment != null) { - txt += attachment.getMedium().getUrl() + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid(); - if (jmsg.getReplies() > 0) { - if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { - txt += " (" + jmsg.getReplies() + " reply)"; - } else { - txt += " (" + jmsg.getReplies() + " replies)"; - } - } - txt += " http://juick.com/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(bot.getJid()); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (attachment != null) { - try { - OobX oob = new OobX(new URI(attachment.getMedium().getUrl())); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - route(ClientMessage.from(msg)); - } - } - } - - @Override - public void close() throws Exception { - if (router != null) { - router.close(); - } - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/XMPPServer.java b/juick-xmpp/src/main/java/com/juick/components/XMPPServer.java deleted file mode 100644 index 6206914b..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/XMPPServer.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components; - -import com.juick.components.s2s.*; -import com.juick.service.UserService; -import com.juick.xmpp.extensions.JuickMessage; -import com.juick.xmpp.extensions.StreamError; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.xmlpull.v1.XmlPullParserException; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.Stanza; -import rocks.xmpp.util.XmppUtils; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.inject.Inject; -import javax.net.ssl.*; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import java.io.*; -import java.net.*; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; - -import static com.juick.components.s2s.Connection.NS_TLS; - -/** - * @author ugnich - */ -@Component -public class XMPPServer implements ConnectionListener, AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(XMPPServer.class); - - @Inject - public ExecutorService service; - @Value("${hostname}") - private Jid jid; - @Value("${s2s_port:5269}") - private int s2sPort; - @Value("${keystore}") - public String keystore; - @Value("${keystore_password}") - public String keystorePassword; - @Value("${broken_ssl_hosts}") - public String[] brokenSSLhosts; - @Value("${banned_hosts}") - public String[] bannedHosts; - - private final List inConnections = new CopyOnWriteArrayList<>(); - private final Map> outConnections = new ConcurrentHashMap<>(); - private final List outCache = new CopyOnWriteArrayList<>(); - private final List stanzaListeners = new CopyOnWriteArrayList<>(); - private final AtomicBoolean closeFlag = new AtomicBoolean(false); - - SSLContext sc; - private TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return null; - } - } - }; - - - private ServerSocket listener; - - @Inject - private BasicXmppSession session; - @Inject - private UserService userService; - - @PostConstruct - public void init() throws KeyStoreException { - closeFlag.set(false); - KeyStore ks = KeyStore.getInstance("JKS"); - try (InputStream ksIs = new FileInputStream(keystore)) { - ks.load(ksIs, keystorePassword.toCharArray()); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory - .getDefaultAlgorithm()); - kmf.init(ks, keystorePassword.toCharArray()); - sc = SSLContext.getInstance("TLSv1.2"); - sc.init(kmf.getKeyManagers(), trustAllCerts, new SecureRandom()); - } catch (Exception e) { - logger.warn("tls unavailable"); - } - service.submit(() -> { - try { - listener = new ServerSocket(s2sPort); - logger.info("s2s listener ready"); - while (!listener.isClosed()) { - if (Thread.currentThread().isInterrupted()) break; - Socket socket = listener.accept(); - ConnectionIn client = new ConnectionIn(this, socket); - addConnectionIn(client); - service.submit(client); - } - } catch (SocketException e) { - // shutdown - } catch (IOException | XmlPullParserException e) { - logger.warn("xmpp exception", e); - } - }); - } - - @Override - public void close() throws Exception { - if (!listener.isClosed()) { - listener.close(); - } - outConnections.forEach((c, s) -> { - c.logoff(); - outConnections.remove(c); - }); - inConnections.forEach(c -> { - c.closeConnection(); - inConnections.remove(c); - }); - if (!listener.isClosed()) { - listener.close(); - } - service.shutdown(); - logger.info("XMPP server destroyed"); - } - - public void addConnectionIn(ConnectionIn c) { - c.setListener(this); - inConnections.add(c); - } - - public void addConnectionOut(ConnectionOut c, Optional socket) { - c.setListener(this); - outConnections.put(c, socket); - } - - public void removeConnectionIn(ConnectionIn c) { - inConnections.remove(c); - } - - public void removeConnectionOut(ConnectionOut c) { - outConnections.remove(c); - } - - public String getFromCache(Jid to) { - final String[] cache = new String[1]; - outCache.stream().filter(c -> c.hostname != null && c.hostname.equals(to)).findFirst().ifPresent(c -> { - cache[0] = c.xml; - outCache.remove(c); - }); - return cache[0]; - } - - public Optional getConnectionOut(Jid hostname, boolean needReady) { - return outConnections.keySet().stream().filter(c -> c.to != null && - c.to.equals(hostname) && (!needReady || c.streamReady)).findFirst(); - } - - public Optional getConnectionIn(String streamID) { - return inConnections.stream().filter(c -> c.streamID != null && c.streamID.equals(streamID)).findFirst(); - } - - public void sendOut(Stanza s) { - try { - StringWriter stanzaWriter = new StringWriter(); - XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( - session.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); - session.createMarshaller().marshal(s, xmppStreamWriter); - xmppStreamWriter.flush(); - xmppStreamWriter.close(); - String xml = stanzaWriter.toString(); - logger.info("s2s (out): {}", xml); - sendOut(Jid.of(s.getTo().getDomain()), xml); - } catch (XMLStreamException | JAXBException e1) { - logger.info("jaxb exception", e1); - } - } - - public void sendOut(Jid hostname, String xml) { - boolean haveAnyConn = false; - - ConnectionOut connOut = null; - for (ConnectionOut c : outConnections.keySet()) { - if (c.to != null && c.to.equals(hostname)) { - if (c.streamReady) { - connOut = c; - break; - } else { - haveAnyConn = true; - break; - } - } - } - if (connOut != null) { - connOut.send(xml); - return; - } - - boolean haveCache = false; - for (CacheEntry c : outCache) { - if (c.hostname != null && c.hostname.equals(hostname)) { - c.xml += xml; - c.updated = Instant.now(); - haveCache = true; - break; - } - } - if (!haveCache) { - outCache.add(new CacheEntry(hostname, xml)); - } - - if (!haveAnyConn && !closeFlag.get()) { - try { - createDialbackConnection(hostname.toEscapedString(), null, null); - } catch (Exception e) { - logger.warn("dialback error", e); - } - } - } - - void createDialbackConnection(String to, String checkSID, String dbKey) throws Exception { - ConnectionOut connectionOut = new ConnectionOut(getJid(), Jid.of(to), null, null, checkSID, dbKey); - addConnectionOut(connectionOut, Optional.empty()); - service.submit(() -> { - try { - Socket socket = new Socket(); - socket.connect(DNSQueries.getServerAddress(to)); - connectionOut.setInputStream(socket.getInputStream()); - connectionOut.setOutputStream(socket.getOutputStream()); - addConnectionOut(connectionOut, Optional.of(socket)); - connectionOut.addChildParser(new JuickMessage()); - connectionOut.connect(); - } catch (IOException e) { - userService.getActiveJIDs().stream().filter(j -> Jid.of(j).getDomain().equals(to)) - .forEach(j -> { - userService.setActiveStatusForJID(j, UserService.ActiveStatus.Inactive); - logger.info("{} is inactive now", j); - }); - } - }); - } - - public void startDialback(Jid from, String streamId, String dbKey) throws Exception { - Optional c = getConnectionOut(from, false); - if (c.isPresent()) { - c.get().sendDialbackVerify(streamId, dbKey); - } else { - createDialbackConnection(from.toEscapedString(), streamId, dbKey); - } - } - - public void addStanzaListener(StanzaListener listener) { - stanzaListeners.add(listener); - } - - public void onStanzaReceived(String xmlValue) { - Stanza stanza = parse(xmlValue); - stanzaListeners.forEach(l -> l.stanzaReceived(stanza)); - } - - public BasicXmppSession getSession() { - return session; - } - - public List getInConnections() { - return inConnections; - } - - public Map> getOutConnections() { - return outConnections; - } - - @Override - public void starttls(ConnectionIn connection) { - logger.info("stream {} securing", connection.streamID); - connection.sendStanza(""); - try { - connection.setSocket(sc.getSocketFactory().createSocket(connection.getSocket(), connection.getSocket().getInetAddress().getHostAddress(), - connection.getSocket().getPort(), true)); - ((SSLSocket) connection.getSocket()).setUseClientMode(false); - ((SSLSocket) connection.getSocket()).startHandshake(); - connection.setSecured(true); - logger.info("stream {} secured", connection.streamID); - connection.restartParser(); - } catch (XmlPullParserException | IOException sex) { - logger.warn("stream {} ssl error {}", connection.streamID, sex); - connection.sendStanza(""); - removeConnectionIn(connection); - connection.closeConnection(); - } - } - - @Override - public void proceed(ConnectionOut connection) { - try { - Socket socket = outConnections.get(connection).get(); - socket = sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(), - socket.getPort(), true); - ((SSLSocket) socket).startHandshake(); - connection.setSecured(true); - logger.info("stream {} secured", connection.getStreamID()); - connection.setInputStream(socket.getInputStream()); - connection.setOutputStream(socket.getOutputStream()); - connection.restartStream(); - connection.sendOpenStream(); - } catch (NoSuchElementException | XmlPullParserException | IOException sex) { - logger.error("s2s ssl error: {} {}, error {}", connection.to, connection.getStreamID(), sex); - connection.send(""); - removeConnectionOut(connection); - connection.logoff(); - } - } - - @Override - public void verify(ConnectionOut connection, String from, String type, String sid) { - if (from != null && from.equals(connection.to.toEscapedString()) && sid != null && !sid.isEmpty() && type != null) { - getConnectionIn(sid).ifPresent(c -> c.sendDialbackResult(Jid.of(from), type)); - } - } - - @Override - public void dialbackError(ConnectionOut connection, StreamError error) { - logger.warn("Stream error from {}: {}", connection.getStreamID(), error.getCondition()); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void finished(ConnectionOut connection, boolean dirty) { - logger.warn("stream to {} {} finished, dirty={}", connection.to, connection.getStreamID(), dirty); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void exception(ConnectionOut connection, Exception ex) { - logger.error("s2s out exception: {} {}, exception {}", connection.to, connection.getStreamID(), ex); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void ready(ConnectionOut connection) { - logger.info("stream to {} {} ready", connection.to, connection.getStreamID()); - String cache = getFromCache(connection.to); - if (cache != null) { - logger.info("stream to {} {} sending cache", connection.to, connection.getStreamID()); - connection.send(cache); - } - } - - @Override - public boolean securing(ConnectionOut connection) { - return !Arrays.asList(brokenSSLhosts).contains(connection.to.toEscapedString()); - } - - public Stanza parse(String xml) { - try { - Unmarshaller unmarshaller = session.createUnmarshaller(); - return (Stanza)unmarshaller.unmarshal(new StringReader(xml)); - } catch (JAXBException e) { - logger.error("JAXB exception", e); - } - return null; - } - - public Jid getJid() { - return jid; - } - @PreDestroy - public void preDestroy() { - closeFlag.set(true); - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/configuration/XmppAppConfiguration.java b/juick-xmpp/src/main/java/com/juick/components/configuration/XmppAppConfiguration.java deleted file mode 100644 index f14b2b23..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/configuration/XmppAppConfiguration.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.configuration; - -/** - * Created by aalexeev on 11/12/16. - */ - -import com.juick.components.s2s.BasicXmppSession; -import com.juick.server.configuration.BaseWebConfiguration; -import com.juick.xmpp.helpers.JidConverter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.core.convert.ConversionService; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import rocks.xmpp.core.session.Extension; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.session.debug.LogbackDebugger; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@Configuration -@ComponentScan(basePackages = {"com.juick.components"}) -@PropertySource("classpath:juick.conf") -@EnableScheduling -@EnableWebMvc -public class XmppAppConfiguration extends BaseWebConfiguration { - @Value("${hostname}") - private String hostname; - - @Bean - public ExecutorService service() { - return Executors.newCachedThreadPool(); - } - @Bean - public BasicXmppSession session() { - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.Message.class)) - .debugger(LogbackDebugger.class) - .build(); - return BasicXmppSession.create(hostname, configuration); - } - @Bean - public static ConversionService conversionService() { - DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); - cs.addConverter(new JidConverter()); - return cs; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/configuration/XmppInitializer.java b/juick-xmpp/src/main/java/com/juick/components/configuration/XmppInitializer.java deleted file mode 100644 index 38dc05d4..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/configuration/XmppInitializer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.configuration; - -import com.juick.configuration.DataConfiguration; -import com.juick.server.configuration.StorageConfiguration; -import org.apache.commons.codec.CharEncoding; -import org.springframework.web.filter.CharacterEncodingFilter; -import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; - -import javax.servlet.Filter; - -/** - * Created by vt on 09/02/16. - */ -public class XmppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected Class[] getRootConfigClasses() { - return new Class[]{ XmppAppConfiguration.class, DataConfiguration.class, StorageConfiguration.class}; - } - - @Override - protected Class[] getServletConfigClasses() { - return null; - } - - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } - - @Override - protected Filter[] getServletFilters() { - CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(CharEncoding.UTF_8); - return new Filter[]{characterEncodingFilter}; - } - - @Override - protected String getServletName() { - return "XMPP dispatcher servlet"; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/controllers/StatusController.java b/juick-xmpp/src/main/java/com/juick/components/controllers/StatusController.java deleted file mode 100644 index f20fc76a..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/controllers/StatusController.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.controllers; - -import com.juick.components.XMPPServer; -import com.juick.components.controllers.helpers.XMPPStatus; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import javax.inject.Inject; - -/** - * Created by vitalyster on 24.10.2016. - */ -@RestController -public class StatusController { - @Inject - private XMPPServer xmpp; - - @RequestMapping(method = RequestMethod.GET, value = "/status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public XMPPStatus status() { - XMPPStatus status = new XMPPStatus(); - if (xmpp != null) { - status.setInbound(xmpp.getInConnections()); - status.setOutbound(xmpp.getOutConnections().keySet()); - } - return status; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/controllers/helpers/XMPPStatus.java b/juick-xmpp/src/main/java/com/juick/components/controllers/helpers/XMPPStatus.java deleted file mode 100644 index c5a7f6e3..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/controllers/helpers/XMPPStatus.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.controllers.helpers; - -import com.juick.components.s2s.ConnectionIn; -import com.juick.components.s2s.ConnectionOut; - -import java.util.List; -import java.util.Set; - -/** - * Created by vitalyster on 16.02.2017. - */ -public class XMPPStatus { - private List inbound; - private Set outbound; - - public List getInbound() { - return inbound; - } - - public void setInbound(List inbound) { - this.inbound = inbound; - } - - public Set getOutbound() { - return outbound; - } - - public void setOutbound(Set outbound) { - this.outbound = outbound; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/BasicXmppSession.java b/juick-xmpp/src/main/java/com/juick/components/s2s/BasicXmppSession.java deleted file mode 100644 index bf352f8c..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/BasicXmppSession.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.ConnectionConfiguration; -import rocks.xmpp.core.session.XmppSession; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.stanza.model.IQ; -import rocks.xmpp.core.stanza.model.Message; -import rocks.xmpp.core.stanza.model.Presence; -import rocks.xmpp.core.stanza.model.server.ServerIQ; -import rocks.xmpp.core.stanza.model.server.ServerMessage; -import rocks.xmpp.core.stanza.model.server.ServerPresence; -import rocks.xmpp.core.stream.model.StreamElement; - -/** - * Created by vitalyster on 06.02.2017. - */ -public class BasicXmppSession extends XmppSession { - protected BasicXmppSession(String xmppServiceDomain, XmppSessionConfiguration configuration, ConnectionConfiguration... connectionConfigurations) { - super(xmppServiceDomain, configuration, connectionConfigurations); - } - - public static BasicXmppSession create(String xmppServiceDomain, XmppSessionConfiguration configuration) { - BasicXmppSession session = new BasicXmppSession(xmppServiceDomain, configuration); - notifyCreationListeners(session); - return session; - } - - @Override - public void connect(Jid from) throws XmppException { - - } - - @Override - public Jid getConnectedResource() { - return null; - } - - @Override - protected StreamElement prepareElement(StreamElement element) { - if (element instanceof Message) { - element = ServerMessage.from((Message) element); - } else if (element instanceof Presence) { - element = ServerPresence.from((Presence) element); - } else if (element instanceof IQ) { - element = ServerIQ.from((IQ) element); - } - - return element; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/CacheEntry.java b/juick-xmpp/src/main/java/com/juick/components/s2s/CacheEntry.java deleted file mode 100644 index c8eeab53..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/CacheEntry.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import rocks.xmpp.addr.Jid; - -import java.time.Instant; - -/** - * - * @author ugnich - */ -public class CacheEntry { - - public Jid hostname; - public Instant created; - public Instant updated; - public String xml; - - public CacheEntry(Jid hostname, String xml) { - this.hostname = hostname; - this.created = this.updated =Instant.now(); - this.xml = xml; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/Connection.java b/juick-xmpp/src/main/java/com/juick/components/s2s/Connection.java deleted file mode 100644 index 8157e046..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/Connection.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.juick.components.XMPPServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.UUID; - -/** - * - * @author ugnich - */ -public class Connection { - - protected static final Logger logger = LoggerFactory.getLogger(Connection.class); - - public String streamID; - public Instant created; - public Instant updated; - public long bytesLocal = 0; - public long packetsLocal = 0; - XMPPServer xmpp; - private Socket socket; - public static final String NS_DB = "jabber:server:dialback"; - public static final String NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls"; - public static final String NS_STREAM = "http://etherx.jabber.org/streams"; - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - XmlPullParser parser = factory.newPullParser(); - OutputStreamWriter writer; - private boolean secured = false; - - - - public Connection(XMPPServer xmpp) throws XmlPullParserException { - this.xmpp = xmpp; - created = updated = Instant.now(); - } - - public void logParser() { - if (streamID == null) { - return; - } - String tag = "IN: <" + parser.getName(); - for (int i = 0; i < parser.getAttributeCount(); i++) { - tag += " " + parser.getAttributeName(i) + "=\"" + parser.getAttributeValue(i) + "\""; - } - tag += ">...\n"; - logger.trace(tag); - } - - public void sendStanza(String xml) { - if (streamID != null) { - logger.trace("OUT: {}\n", xml); - } - try { - writer.write(xml); - writer.flush(); - } catch (IOException e) { - logger.error("send stanza failed", e); - } - - updated = Instant.now(); - bytesLocal += xml.length(); - packetsLocal++; - } - - public void closeConnection() { - if (streamID != null) { - logger.info("closing stream {}", streamID); - } - - try { - writer.write(""); - } catch (Exception e) { - } - - try { - writer.close(); - } catch (Exception e) { - } - - try { - socket.close(); - } catch (Exception e) { - } - } - - public boolean isSecured() { - return secured; - } - - public void setSecured(boolean secured) { - this.secured = secured; - } - - public void restartParser() throws XmlPullParserException, IOException { - streamID = UUID.randomUUID().toString(); - parser = factory.newPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(new InputStreamReader(socket.getInputStream())); - writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8); - } - - @JsonIgnore - public Socket getSocket() { - return socket; - } - - public void setSocket(Socket socket) { - this.socket = socket; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionIn.java b/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionIn.java deleted file mode 100644 index 16f207a7..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionIn.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import com.juick.components.XMPPServer; -import com.juick.xmpp.extensions.StreamError; -import com.juick.xmpp.utils.XmlUtils; -import org.apache.commons.lang3.StringUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import rocks.xmpp.addr.Jid; - -import java.io.EOFException; -import java.io.IOException; -import java.net.Socket; -import java.net.SocketException; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * @author ugnich - */ -public class ConnectionIn extends Connection implements Runnable { - - final public List from = new CopyOnWriteArrayList<>(); - public Instant received; - public long packetsRemote = 0; - ConnectionListener listener; - - public ConnectionIn(XMPPServer xmpp, Socket socket) throws XmlPullParserException, IOException { - super(xmpp); - this.setSocket(socket); - restartParser(); - } - - @Override - public void run() { - try { - parser.next(); // stream:stream - updateTsRemoteData(); - if (!parser.getName().equals("stream") - || !parser.getNamespace("stream").equals(NS_STREAM)) { -// || !parser.getAttributeValue(null, "version").equals("1.0") -// || !parser.getAttributeValue(null, "to").equals(Main.HOSTNAME)) { - throw new Exception(String.format("stream from %s invalid", getSocket().getRemoteSocketAddress())); - } - streamID = parser.getAttributeValue(null, "id"); - if (streamID == null) { - streamID = UUID.randomUUID().toString(); - } - boolean xmppversionnew = parser.getAttributeValue(null, "version") != null; - String from = parser.getAttributeValue(null, "from"); - - if (Arrays.asList(xmpp.bannedHosts).contains(from)) { - closeConnection(); - return; - } - sendOpenStream(from, xmppversionnew); - - while (parser.next() != XmlPullParser.END_DOCUMENT) { - updateTsRemoteData(); - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - logParser(); - - packetsRemote++; - - String tag = parser.getName(); - if (tag.equals("result") && parser.getNamespace().equals(NS_DB)) { - String dfrom = parser.getAttributeValue(null, "from"); - String to = parser.getAttributeValue(null, "to"); - logger.info("stream from {} to {} {} asking for dialback", dfrom, to, streamID); - if (dfrom.endsWith(xmpp.getJid().toEscapedString()) && (dfrom.equals(xmpp.getJid().toEscapedString()) - || dfrom.endsWith("." + xmpp.getJid()))) { - logger.warn("stream from {} is invalid", dfrom); - break; - } - if (to != null && to.equals(xmpp.getJid().toEscapedString())) { - String dbKey = XmlUtils.getTagText(parser); - updateTsRemoteData(); - xmpp.startDialback(Jid.of(dfrom), streamID, dbKey); - } else { - logger.warn("stream from " + dfrom + " " + streamID + " invalid to " + to); - break; - } - } else if (tag.equals("verify") && parser.getNamespace().equals(NS_DB)) { - String vfrom = parser.getAttributeValue(null, "from"); - String vto = parser.getAttributeValue(null, "to"); - String vid = parser.getAttributeValue(null, "id"); - String vkey = XmlUtils.getTagText(parser); - updateTsRemoteData(); - final boolean[] valid = {false}; - if (vfrom != null && vto != null && vid != null && vkey != null) { - xmpp.getConnectionOut(Jid.of(vfrom), false).ifPresent(c -> { - String dialbackKey = c.dbKey; - valid[0] = vkey.equals(dialbackKey); - }); - } - if (valid[0]) { - sendStanza(""); - logger.info("stream from {} {} dialback verify valid", vfrom, streamID); - } else { - sendStanza(""); - logger.warn("stream from {} {} dialback verify invalid", vfrom, streamID); - } - } else if (tag.equals("presence") && checkFromTo(parser)) { - String xml = XmlUtils.parseToString(parser, false); - logger.info("stream {} presence: {}", streamID, xml); - xmpp.onStanzaReceived(xml); - } else if (tag.equals("message") && checkFromTo(parser)) { - updateTsRemoteData(); - String xml = XmlUtils.parseToString(parser, false); - logger.info("stream {} message: {}", streamID, xml); - xmpp.onStanzaReceived(xml); - - } else if (tag.equals("iq") && checkFromTo(parser)) { - updateTsRemoteData(); - String type = parser.getAttributeValue(null, "type"); - String xml = XmlUtils.parseToString(parser, false); - if (type == null || !type.equals("error")) { - logger.info("stream {} iq: {}", streamID, xml); - xmpp.onStanzaReceived(xml); - } - } else if (!isSecured() && tag.equals("starttls")) { - listener.starttls(this); - } else if (isSecured() && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) { - sendOpenStream(null, true); - } else if (tag.equals("error")) { - StreamError streamError = StreamError.parse(parser); - logger.warn("Stream error from {}: {}", streamID, streamError.getCondition()); - xmpp.removeConnectionIn(this); - closeConnection(); - } else { - String unhandledStanza = XmlUtils.parseToString(parser, true); - logger.warn("Unhandled stanza from {}: {}", streamID, unhandledStanza); - } - } - logger.warn("stream {} finished", streamID); - xmpp.removeConnectionIn(this); - closeConnection(); - } catch (EOFException | SocketException ex) { - logger.info("stream {} closed (dirty)", streamID); - xmpp.removeConnectionIn(this); - closeConnection(); - } catch (Exception e) { - logger.warn("stream {} error {}", streamID, e); - xmpp.removeConnectionIn(this); - closeConnection(); - } - } - - void updateTsRemoteData() { - received = Instant.now(); - } - - void sendOpenStream(String from, boolean xmppversionnew) throws IOException { - String openStream = ""; - if (xmppversionnew) { - openStream += ""; - if (listener != null && !isSecured() && !Arrays.asList(xmpp.brokenSSLhosts).contains(from)) { - openStream += ""; - } - openStream += ""; - } - sendStanza(openStream); - } - - public void sendDialbackResult(Jid sfrom, String type) { - sendStanza(""); - if (type.equals("valid")) { - from.add(sfrom); - logger.info("stream from {} {} ready", sfrom, streamID); - } - } - - boolean checkFromTo(XmlPullParser parser) throws Exception { - String cfrom = parser.getAttributeValue(null, "from"); - String cto = parser.getAttributeValue(null, "to"); - if (StringUtils.isNotEmpty(cfrom) && StringUtils.isNotEmpty(cto)) { - Jid jidto = Jid.of(cto); - if (jidto.getDomain().equals(xmpp.getJid().toEscapedString())) { - Jid jidfrom = Jid.of(cfrom); - for (Jid aFrom : from) { - if (aFrom.equals(Jid.of(jidfrom.getDomain()))) { - return true; - } - } - } - } - return false; - } - public void setListener(ConnectionListener listener) { - this.listener = listener; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionListener.java b/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionListener.java deleted file mode 100644 index 3b191974..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.juick.components.s2s; - -import com.juick.xmpp.extensions.StreamError; - -public interface ConnectionListener { - void starttls(ConnectionIn connection); - void proceed(ConnectionOut connection); - void verify(ConnectionOut connection, String from, String type, String sid); - void dialbackError(ConnectionOut connection, StreamError error); - void finished(ConnectionOut connection, boolean dirty); - void exception(ConnectionOut connection, Exception ex); - void ready(ConnectionOut connection); - boolean securing(ConnectionOut connection); -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionOut.java b/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionOut.java deleted file mode 100644 index 9578a831..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/ConnectionOut.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import com.juick.components.s2s.util.DialbackUtils; -import com.juick.xmpp.Stream; -import com.juick.xmpp.extensions.StreamError; -import com.juick.xmpp.extensions.StreamFeatures; -import com.juick.xmpp.utils.XmlUtils; -import org.apache.commons.text.RandomStringGenerator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xmlpull.v1.XmlPullParser; -import rocks.xmpp.addr.Jid; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.SocketException; -import java.util.UUID; - -/** - * @author ugnich - */ -public class ConnectionOut extends Stream { - protected static final Logger logger = LoggerFactory.getLogger(ConnectionOut.class); - public static final String NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls"; - public static final String NS_DB = "jabber:server:dialback"; - private boolean secured = false; - - public boolean streamReady = false; - String checkSID = null; - String dbKey = null; - private String streamID; - ConnectionListener listener; - RandomStringGenerator generator = new RandomStringGenerator.Builder().withinRange('a', 'z').build(); - - public ConnectionOut(Jid from, Jid to, InputStream is, OutputStream os, String checkSID, String dbKey) throws Exception { - super(from, to, is, os); - this.to = to; - this.checkSID = checkSID; - this.dbKey = dbKey; - if (dbKey == null) { - this.dbKey = DialbackUtils.generateDialbackKey(generator.generate(15), to, from, streamID); - } - streamID = UUID.randomUUID().toString(); - } - - public void sendOpenStream() throws IOException { - send(""); - } - - void processDialback() throws Exception { - if (checkSID != null) { - sendDialbackVerify(checkSID, dbKey); - } - send("" + - dbKey + ""); - } - - @Override - public void handshake() { - try { - restartStream(); - - sendOpenStream(); - - parser.next(); // stream:stream - streamID = parser.getAttributeValue(null, "id"); - if (streamID == null || streamID.isEmpty()) { - throw new Exception("stream to " + to + " invalid first packet"); - } - - logger.info("stream to {} {} open", to, streamID); - boolean xmppversionnew = parser.getAttributeValue(null, "version") != null; - if (!xmppversionnew) { - processDialback(); - } - - while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - String tag = parser.getName(); - if (tag.equals("result") && parser.getNamespace().equals(NS_DB)) { - String type = parser.getAttributeValue(null, "type"); - if (type != null && type.equals("valid")) { - streamReady = true; - listener.ready(this); - } else { - logger.info("stream to {} {} dialback fail", to, streamID); - } - XmlUtils.skip(parser); - } else if (tag.equals("verify") && parser.getNamespace().equals(NS_DB)) { - String from = parser.getAttributeValue(null, "from"); - String type = parser.getAttributeValue(null, "type"); - String sid = parser.getAttributeValue(null, "id"); - listener.verify(this, from, type, sid); - XmlUtils.skip(parser); - } else if (tag.equals("features") && parser.getNamespace().equals(NS_STREAM)) { - StreamFeatures features = StreamFeatures.parse(parser); - if (listener != null && !secured && features.STARTTLS >= 0 - && listener.securing(this)) { - logger.info("stream to {} {} securing", to.toEscapedString(), streamID); - send(""); - } else { - processDialback(); - } - } else if (tag.equals("proceed") && parser.getNamespace().equals(NS_TLS)) { - listener.proceed(this); - } else if (secured && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) { - streamID = parser.getAttributeValue(null, "id"); - } else if (tag.equals("error")) { - StreamError streamError = StreamError.parse(parser); - listener.dialbackError(this, streamError); - } else { - String unhandledStanza = XmlUtils.parseToString(parser, true); - logger.warn("Unhandled stanza from {} {} : {}", to, streamID, unhandledStanza); - } - } - listener.finished(this, false); - } catch (EOFException | SocketException eofex) { - listener.finished(this, true); - } catch (Exception e) { - listener.exception(this, e); - } - } - - public void sendDialbackVerify(String sid, String key) { - send("" + - key + ""); - } - public void setListener(ConnectionListener listener) { - this.listener = listener; - } - - public String getStreamID() { - return streamID; - } - - public boolean isSecured() { - return secured; - } - - public void setSecured(boolean secured) { - this.secured = secured; - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/DNSQueries.java b/juick-xmpp/src/main/java/com/juick/components/s2s/DNSQueries.java deleted file mode 100644 index 0cd356b0..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/DNSQueries.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - -import org.apache.commons.lang3.math.NumberUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Hashtable; -import java.util.Random; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; - -/** - * - * @author ugnich - */ -public class DNSQueries { - - private static final Logger logger = LoggerFactory.getLogger(DNSQueries.class); - - private static Random rand = new Random(); - - public static InetSocketAddress getServerAddress(String hostname) { - - String host = hostname; - int port = 5269; - - Hashtable env = new Hashtable<>(5); - env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); - try { - DirContext ctx = new InitialDirContext(env); - Attribute att = ctx.getAttributes("_xmpp-server._tcp." + hostname, new String[]{"SRV"}).get("SRV"); - - if (att != null && att.size() > 0) { - int i = rand.nextInt(att.size()); - String srv[] = att.get(i).toString().split(" "); - port = NumberUtils.toInt(srv[2], 5269); - host = srv[3]; - } - ctx.close(); - } catch (NamingException e) { - logger.info("SRV record for {} is not resolved, falling back to A record", hostname); - } - return new InetSocketAddress(host, port); - } -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/StanzaListener.java b/juick-xmpp/src/main/java/com/juick/components/s2s/StanzaListener.java deleted file mode 100644 index 39253d3f..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/StanzaListener.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s; - - -import rocks.xmpp.core.stanza.model.Stanza; - -/** - * Created by vitalyster on 07.12.2016. - */ -public interface StanzaListener { - void stanzaReceived(Stanza xmlValue); -} diff --git a/juick-xmpp/src/main/java/com/juick/components/s2s/util/DialbackUtils.java b/juick-xmpp/src/main/java/com/juick/components/s2s/util/DialbackUtils.java deleted file mode 100644 index fc08c5d6..00000000 --- a/juick-xmpp/src/main/java/com/juick/components/s2s/util/DialbackUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ - -package com.juick.components.s2s.util; - -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.codec.digest.HmacUtils; -import rocks.xmpp.addr.Jid; - -/** - * Created by vitalyster on 05.12.2016. - */ -public class DialbackUtils { - private DialbackUtils() { - throw new IllegalStateException(); - } - - public static String generateDialbackKey(String secret, Jid to, Jid from, String id) { - return HmacUtils.hmacSha256Hex(DigestUtils.sha256(secret), - (to.toEscapedString() + " " + from.toEscapedString() + " " + id).getBytes()); - } -} diff --git a/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickMessage.java b/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickMessage.java deleted file mode 100644 index c28eee14..00000000 --- a/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickMessage.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Juick - * Copyright (C) 2008-2011, Ugnich Anton - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ -package com.juick.xmpp.extensions; - -import com.juick.Tag; -import com.juick.xmpp.StanzaChild; -import com.juick.xmpp.utils.XmlUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.text.StringEscapeUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; -/** - * - * @author Ugnich Anton - */ -public class JuickMessage extends com.juick.Message implements StanzaChild { - public final static String XMLNS = "http://juick.com/message"; - public final static String TagName = "juick"; - private SimpleDateFormat df; - public JuickMessage() { - df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - df.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - @Override - public String getXMLNS() { - return XMLNS; - } - @Override - public JuickMessage parse(XmlPullParser parser) throws XmlPullParserException, IOException, ParseException { - JuickMessage jmsg = new JuickMessage(); - final String sMID = parser.getAttributeValue(null, "mid"); - if (sMID != null) { - jmsg.setMid(Integer.parseInt(sMID)); - } - final String sRID = parser.getAttributeValue(null, "rid"); - if (sRID != null) { - jmsg.setRid(Integer.parseInt(sRID)); - } - final String sReplyTo = parser.getAttributeValue(null, "replyto"); - if (sReplyTo != null) { - jmsg.setReplyto(Integer.parseInt(sReplyTo)); - } - final String sPrivacy = parser.getAttributeValue(null, "privacy"); - if (sPrivacy != null) { - jmsg.setPrivacy(Integer.parseInt(sPrivacy)); - } - final String sFriendsOnly = parser.getAttributeValue(null, "friendsonly"); - if (sFriendsOnly != null) { - jmsg.FriendsOnly = true; - } - final String sReadOnly = parser.getAttributeValue(null, "readonly"); - if (sReadOnly != null) { - jmsg.ReadOnly = true; - } - jmsg.setTimestamp(df.parse(parser.getAttributeValue(null, "ts")).toInstant()); - jmsg.setAttachmentType(parser.getAttributeValue(null, "attach")); - while (parser.next() == XmlPullParser.START_TAG) { - final String tag = parser.getName(); - final String xmlns = parser.getNamespace(); - if (tag.equals("body")) { - jmsg.setText(XmlUtils.getTagText(parser)); - } else if (tag.equals(JuickUser.TagName) && xmlns != null && xmlns.equals(JuickUser.XMLNS)) { - jmsg.setUser(new JuickUser().parse(parser)); - } else if (tag.equals("tag")) { - jmsg.getTags().add(new Tag(XmlUtils.getTagText(parser))); - } else { - XmlUtils.skip(parser); - } - } - return jmsg; - } - @Override - public String toString() { - StringBuilder ret = new StringBuilder("<").append(TagName).append(" xmlns=\"").append(XMLNS).append("\""); - if (getMid() > 0) { - ret.append(" mid=\"").append(getMid()).append("\""); - } - if (getRid() > 0) { - ret.append(" rid=\"").append(getRid()).append("\""); - } - if (getReplyto() > 0) { - ret.append(" replyto=\"").append(getReplyto()).append("\""); - } - ret.append(" privacy=\"").append(getPrivacy()).append("\""); - if (FriendsOnly) { - ret.append(" friendsonly=\"1\""); - } - if (ReadOnly) { - ret.append(" readonly=\"1\""); - } - if (getTimestamp() != null) { - ret.append(" ts=\"").append(df.format(Date.from(getTimestamp()))).append("\""); - } - if (getAttachmentType() != null) { - ret.append(" attach=\"").append(getAttachmentType()).append("\""); - } - ret.append(">"); - if (getUser() != null) { - ret.append(JuickUser.toString(getUser())); - } - if (getText() != null) { - ret.append("").append(StringEscapeUtils.escapeXml10(StringUtils.defaultString(getText()))).append(""); - } - for (Tag Tag : getTags()) { - ret.append("").append(StringEscapeUtils.escapeXml10(Tag.getName())).append(""); - } - ret.append(""); - return ret.toString(); - } - @Override - public boolean equals(Object obj) { - if (!(obj instanceof JuickMessage)) { - return false; - } - JuickMessage jmsg = (JuickMessage) obj; - return (this.getMid() == jmsg.getMid() && this.getRid() == jmsg.getRid()); - } - @Override - public int compareTo(Object obj) throws ClassCastException { - if (!(obj instanceof JuickMessage)) { - throw new ClassCastException(); - } - JuickMessage jmsg = (JuickMessage) obj; - if (this.getMid() != jmsg.getMid()) { - if (this.getMid() > jmsg.getMid()) { - return -1; - } else { - return 1; - } - } - if (this.getRid() != jmsg.getRid()) { - if (this.getRid() < jmsg.getRid()) { - return -1; - } else { - return 1; - } - } - return 0; - } -} \ No newline at end of file diff --git a/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickUser.java b/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickUser.java deleted file mode 100644 index 10d2b564..00000000 --- a/juick-xmpp/src/main/java/com/juick/xmpp/extensions/JuickUser.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Juick - * Copyright (C) 2008-2011, Ugnich Anton - * - * This program 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, or (at your option) any later version. - * - * This program 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 . - */ -package com.juick.xmpp.extensions; -import com.juick.xmpp.StanzaChild; -import com.juick.xmpp.utils.XmlUtils; -import org.apache.commons.text.StringEscapeUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -/** - * - * @author Ugnich Anton - */ -public class JuickUser extends com.juick.User implements StanzaChild { - public final static String XMLNS = "http://juick.com/user"; - public final static String TagName = "user"; - public JuickUser() { - } - @Override - public String getXMLNS() { - return XMLNS; - } - @Override - public JuickUser parse(final XmlPullParser parser) throws XmlPullParserException, IOException { - JuickUser juser = new JuickUser(); - String strUID = parser.getAttributeValue(null, "uid"); - if (strUID != null) { - juser.setUid(Integer.parseInt(strUID)); - } - juser.setName(parser.getAttributeValue(null, "uname")); - XmlUtils.skip(parser); - return juser; - } - public static String toString(com.juick.User user) { - String str = "<" + TagName + " xmlns='" + XMLNS + "'"; - if (user.getUid() > 0) { - str += " uid='" + user.getUid() + "'"; - } - if (user.getName() != null && user.getName().length() > 0) { - str += " uname='" + StringEscapeUtils.escapeXml10(user.getName()) + "'"; - } - str += "/>"; - return str; - } - @Override - public String toString() { - return toString(this); - } -} \ No newline at end of file diff --git a/juick-xmpp/src/main/java/com/juick/xmpp/helpers/JidConverter.java b/juick-xmpp/src/main/java/com/juick/xmpp/helpers/JidConverter.java deleted file mode 100644 index 253c50f8..00000000 --- a/juick-xmpp/src/main/java/com/juick/xmpp/helpers/JidConverter.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.juick.xmpp.helpers; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; -import rocks.xmpp.addr.Jid; - -public class JidConverter implements Converter { - @Nullable - @Override - public Jid convert(String jidStr) { - return Jid.of(jidStr); - } -} diff --git a/juick-xmpp/src/main/resources/juick.png b/juick-xmpp/src/main/resources/juick.png deleted file mode 100644 index c85ef2c4..00000000 Binary files a/juick-xmpp/src/main/resources/juick.png and /dev/null differ diff --git a/juick-xmpp/src/main/webapp/WEB-INF/web.xml b/juick-xmpp/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index a57cceb9..00000000 --- a/juick-xmpp/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/juick-xmpp/src/test/java/com/juick/xmpp/server/XMPPServerTests.java b/juick-xmpp/src/test/java/com/juick/xmpp/server/XMPPServerTests.java deleted file mode 100644 index 7c5a1a98..00000000 --- a/juick-xmpp/src/test/java/com/juick/xmpp/server/XMPPServerTests.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.juick.xmpp.server; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.juick.Tag; -import com.juick.User; -import com.juick.components.JuickBot; -import com.juick.components.XMPPServer; -import com.juick.components.configuration.XmppAppConfiguration; -import com.juick.configuration.RepositoryConfiguration; -import com.juick.service.MessagesService; -import com.juick.service.SubscriptionService; -import com.juick.service.TagService; -import com.juick.service.UserService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.Stanza; -import rocks.xmpp.core.stanza.model.server.ServerMessage; - -import javax.inject.Inject; -import java.lang.reflect.InvocationTargetException; -import java.text.ParseException; -import java.util.Collections; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringJUnit4ClassRunner.class) -@WebAppConfiguration -@ContextConfiguration(classes = { - XmppAppConfiguration.class, RepositoryConfiguration.class -}) -@TestPropertySource(properties = {"broken_ssl_hosts=localhost,serverstorageisfull.tld"}) -public class XMPPServerTests extends AbstractJUnit4SpringContextTests { - @Inject - private WebApplicationContext wac; - @Inject - private XMPPServer server; - @Inject - private JuickBot bot; - @Inject - private UserService userService; - @Inject - private MessagesService messagesService; - @Inject - private TagService tagService; - @Inject - private SubscriptionService subscriptionService; - @Inject - private JdbcTemplate jdbcTemplate; - @Value("${hostname}") - private Jid jid; - - private MockMvc mockMvc; - - @Before - public void setup() { - mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); - } - @Test - public void statusPageIsUp() throws Exception { - mockMvc.perform(get("http://localhost:8080/status")).andExpect(status().isOk()); - assertThat(server.getJid(), equalTo(jid)); - } - @Test - public void botIsUpAndProcessingResourceConstraints() { - int renhaId; - renhaId = userService.createUser("renha", "umnnbt"); - Jid from = Jid.of("renha@serverstorageisfull.tld"); - jdbcTemplate.update("INSERT INTO jids(user_id,jid,active) VALUES(?,?,?)", renhaId, from.toEscapedString(), 1); - String xmlMessage = "Reply by @LexXПохоже нынче можно публично заявлять о своем веганстве. Your contact offline message queue is full. The message has been discarded."; - Stanza msg = server.parse(xmlMessage); - assertThat(from, equalTo(msg.getFrom())); - boolean isActive = jdbcTemplate.queryForObject("SELECT active FROM jids WHERE user_id=?", Integer.class, renhaId) == 1; - assertThat(isActive, equalTo(true)); - bot.incomingMessage((ServerMessage)msg); - isActive = jdbcTemplate.queryForObject("SELECT active FROM jids WHERE user_id=?", Integer.class, renhaId) == 1; - assertThat(isActive, equalTo(false)); - } - @Test - public void botCommandsTests() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { - assertThat(bot.processCommand(new User(), Jid.of("test@localhost"), "PING").get(), is("PONG")); - // subscription commands have two lines, others have 1 - assertThat(bot.processCommand(new User(), Jid.of("test@localhost"), "help").get().split("\n").length, is(23)); - } - - @Test - public void protocolTests() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ParseException, JsonProcessingException { - int uid = userService.createUser("me", "secret"); - User user = userService.getUserByUID(uid).orElse(new User()); - Tag yo = tagService.getTag("yo", true); - int mid = messagesService.createMessage(uid, "yoyo", null, Collections.singletonList(yo)); - assertEquals("should be message", true, - bot.processCommand(user, Jid.of("test@localhost"), String.format("#%d", mid)).get().startsWith("@me")); - mid = messagesService.getUserBlog(user.getUid(), -1, 0).stream().reduce((first, second) -> second).get(); - assertEquals("text should match", "yoyo", - messagesService.getMessage(mid).getText()); - assertEquals("tag should match", "yo", - tagService.getMessageTags(mid).get(0).getTag().getName()); - int readerUid = userService.createUser("dummyReader", "dummySecret"); - User readerUser = userService.getUserByUID(readerUid).orElse(new User()); - assertEquals("should be subscribed", "Subscribed", - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "S #" + mid).get()); - /* TODO: move from juick-legacy - assertEquals("should be favorited", "Message added to your recommendations", - juickProtocol.getReply(readerUser, "! #" + mid)); - */ - assertEquals("number of subscribed users should match", 1, - subscriptionService.getUsersSubscribedToComments(mid, uid).size()); - /* - assertEquals("should be subscribed", "Subscribed", - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "S @" + user.getName()).get()); - List friends = userService.getUserFriends(readerUid); - assertEquals("number of friend users should match", 2, - friends.size()); - assertEquals("number of reader users should match", 1, - userService.getUserReaders(uid).size()); - String expectedReply = "Reply posted.\n#" + mid + "/1 " - + "http://juick.com/" + mid + "#1"; - String expectedSecondReply = "Reply posted.\n#" + mid + "/2 " - + "http://juick.com/" + mid + "#2"; - assertEquals("should be reply", expectedReply, - bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " yoyo").get()); - assertEquals("should be second reply", expectedSecondReply, - bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + "/1 yoyo").get()); - Message reply = messagesService.getReplies(mid).stream().filter(m -> m.getRid() == 2).findFirst() - .orElse(new Message()); - assertEquals("should be reply to first comment", 1, reply.getReplyto()); - assertNotEquals("tags should NOT be updated", "Tags are updated", - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "#" + mid + " *yo *there").get()); - assertEquals("tags should be updated", "Tags are updated", - bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " *there").get()); - assertEquals("number of tags should match", 2, - tagService.getMessageTags(mid).size()); - assertEquals("should be blacklisted", "Tag added to your blacklist", - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "BL *there").get()); - assertEquals("number of subscribed users should match", 0, - subscriptionService.getSubscribedUsers(uid, mid).size()); - assertEquals("tags should be updated", "Tags are updated", - bot.processCommand(user, Jid.of("test@localhost"), "#" + mid + " *there").get()); - assertEquals("number of tags should match", 1, - tagService.getMessageTags(mid).size()); - int taggerUid = userService.createUser("dummyTagger", "dummySecret"); - User taggerUser = userService.getUserByUID(taggerUid).orElse(new User()); - assertEquals("should be subscribed", "Subscribed", - bot.processCommand(taggerUser, Jid.of("tagger@localhost"), "S *yo").get()); - assertEquals("number of subscribed users should match", 2, - subscriptionService.getSubscribedUsers(uid, mid).size()); - assertEquals("should be unsubscribed", "Unsubscribed from yo", - bot.processCommand(taggerUser, Jid.of("tagger@localhost"), "U *yo").get()); - assertEquals("number of subscribed users should match", 1, - subscriptionService.getSubscribedUsers(uid, mid).size()); - assertEquals("number of readers should match", 1, - userService.getUserReaders(uid).size()); - String readerFeed = bot.processCommand(readerUser, Jid.of("dummy@localhost"), "#").get(); - assertEquals("description should match", true, readerFeed.startsWith("Your feed")); - assertEquals("should be unsubscribed", "Unsubscribed from @" + user.getName(), - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "U @" + user.getName()).get()); - assertEquals("number of readers should match", 0, - userService.getUserReaders(uid).size()); - assertEquals("number of friends should match", 1, - userService.getUserFriends(uid).size()); - assertEquals("should be unsubscribed", "Unsubscribed from #" + mid, - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "u #" + mid).get()); - assertEquals("number of subscribed users should match", 0, - subscriptionService.getUsersSubscribedToComments(mid, uid).size()); - assertNotEquals("should NOT be deleted", String.format("Message %s deleted", mid), - bot.processCommand(readerUser, Jid.of("dummy@localhost"), "D #" + mid).get()); - assertEquals("should be deleted", String.format("Message %s deleted", mid), - bot.processCommand(user, Jid.of("test@localhost"), "D #" + mid).get()); - assertEquals("should not have messages", 0, messagesService.getAll(user.getUid(), 0).size()); - */ - } -} diff --git a/settings.gradle b/settings.gradle index 07848f53..005d592b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ rootProject.name = "Juick" -include ':juick-core', ':juick-server-core', ':juick-server-jdbc', ':juick-server-web', ':juick-server', ':juick-www', ':juick-rss', ':juick-notifications', ':juick-xmpp', ':juick-xmpp-wip' +include ':juick-core', ':juick-server-core', ':juick-server-jdbc', ':juick-server-web', ':juick-server', ':juick-www', ':juick-rss', ':juick-notifications', ':juick-xmpp-wip' -- cgit v1.2.3