aboutsummaryrefslogtreecommitdiff
path: root/juick-server/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'juick-server/src/main/java/com')
-rw-r--r--juick-server/src/main/java/com/juick/server/NotificationListener.java18
-rw-r--r--juick-server/src/main/java/com/juick/server/XMPPConnection.java673
-rw-r--r--juick-server/src/main/java/com/juick/server/XMPPServer.java413
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Messages.java19
-rw-r--r--juick-server/src/main/java/com/juick/server/api/PM.java5
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Post.java288
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Service.java117
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/ApiAppConfiguration.java25
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java124
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/JidConverter.java13
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java48
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java10
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/iq/package-info.java8
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/Stream.java184
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java58
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/StreamError.java44
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/StreamHandler.java13
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java10
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/XMPPError.java73
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java189
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/router/XmlUtils.java88
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java69
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java40
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/Connection.java139
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java213
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java15
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java167
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java65
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java28
-rw-r--r--juick-server/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java37
30 files changed, 2757 insertions, 436 deletions
diff --git a/juick-server/src/main/java/com/juick/server/NotificationListener.java b/juick-server/src/main/java/com/juick/server/NotificationListener.java
new file mode 100644
index 00000000..f6330570
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/NotificationListener.java
@@ -0,0 +1,18 @@
+package com.juick.server;
+
+import com.juick.server.component.LikeEvent;
+import com.juick.server.component.MessageEvent;
+import com.juick.server.component.PingEvent;
+import com.juick.server.component.SubscribeEvent;
+import org.springframework.context.event.EventListener;
+
+public interface NotificationListener {
+ @EventListener
+ void processMessageEvent(MessageEvent messageEvent);
+ @EventListener
+ void processSubscribeEvent(SubscribeEvent subscribeEvent);
+ @EventListener
+ void processLikeEvent(LikeEvent likeEvent);
+ @EventListener
+ void ProcessPingEvent(PingEvent pingEvent);
+}
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..59b33aba
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/XMPPConnection.java
@@ -0,0 +1,673 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server;
+
+import com.juick.User;
+import com.juick.server.component.LikeEvent;
+import com.juick.server.component.MessageEvent;
+import com.juick.server.component.PingEvent;
+import com.juick.server.component.SubscribeEvent;
+import com.juick.server.helpers.CommandResult;
+import com.juick.server.helpers.UserInfo;
+import com.juick.server.xmpp.iq.MessageQuery;
+import com.juick.server.xmpp.s2s.BasicXmppSession;
+import com.juick.server.xmpp.s2s.StanzaListener;
+import com.juick.service.*;
+import com.juick.util.MessageUtils;
+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.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+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.*;
+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 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.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+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.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * @author ugnich
+ */
+@Component
+@DependsOn("XMPPRouter")
+public class XMPPConnection implements StanzaListener, NotificationListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(XMPPConnection.class);
+
+ private ExternalComponent router;
+ @Inject
+ private XMPPServer xmpp;
+ @Inject
+ private CommandsManager commandsManager;
+ @Value("${xmppbot_jid:juick@localhost}")
+ private Jid jid;
+ @Value("${componentname:localhost}")
+ private String componentName;
+ @Value("${component_port:5347}")
+ private int componentPort;
+ @Value("${xmpp_password:secret}")
+ private String password;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String imgDir;
+
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private TagService tagService;
+ @Inject
+ private BasicXmppSession session;
+ @Inject
+ private ExecutorService service;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ @PostConstruct
+ public void init() {
+ logger.info("stream router start connecting to {}", componentPort);
+ xmpp.addStanzaListener(this);
+ 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(MessageQuery.class, iq -> {
+ Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT);
+ warningMessage.setFrom(jid);
+ warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that");
+ router.send(warningMessage);
+ return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam"));
+ });
+ router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) {
+ @Override
+ protected IQ processRequest(IQ iq) {
+ if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid())
+ || iq.getTo().asBareJid().toEscapedString().equals(jid.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 -> {
+ ClientMessage result = incomingMessage(e.getMessage());
+ if (result != null) {
+ router.send(result);
+ }
+ });
+ router.addInboundIQListener(e -> {
+ IQ iq = e.getIQ();
+ Jid jid = iq.getTo();
+ if (!jid.getDomain().equals(this.jid.getDomain())) {
+ router.send(iq);
+ }
+ });
+ FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class);
+ fileTransferManager.addFileTransferOfferListener(e -> {
+ try {
+ List<String> allowedTypes = new ArrayList<String>() {{
+ 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");
+ try {
+ Jid initiator = e.getInitiator();
+ ClientMessage result = incomingMessageJuick(
+ userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator,
+ e.getDescription(), URI.create(String.format("juick://%s", targetFilename)));
+ if (result != null) {
+ router.send(result);
+ }
+ } catch (Exception e1) {
+ logger.error("ft 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(jid);
+ 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 {
+ router.connect();
+ broadcastPresence(null);
+ } catch (XmppException e) {
+ logger.warn("xmpp exception", e);
+ }
+ });
+ }
+
+ private 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();
+ }
+
+ private void sendJuickMessage(com.juick.Message jmsg, List<User> users) {
+ List<String> jids = new ArrayList<>();
+
+ if (jmsg.FriendsOnly) {
+ jids = subscriptionService.getJIDSubscribedToUser(jmsg.getUser().getUid(), jmsg.FriendsOnly);
+ } else {
+ for (User user : users) {
+ jids.addAll(userService.getJIDsbyUID(user.getUid()));
+ }
+ }
+ com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid());
+ String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n";
+ String attachmentUrl = MessageUtils.attachmentUrl(fullMsg);
+ if (StringUtils.isNotEmpty(attachmentUrl)) {
+ txt += attachmentUrl + "\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(jid);
+ msg.setBody(txt);
+ msg.setType(Message.Type.CHAT);
+ msg.setThread("juick-" + jmsg.getMid());
+ msg.addExtension(jmsg);
+ msg.addExtension(nick);
+ if (StringUtils.isNotEmpty(attachmentUrl)) {
+ try {
+ OobX oob = new OobX(new URI(attachmentUrl));
+ msg.addExtension(oob);
+ } catch (URISyntaxException e) {
+ logger.warn("uri exception", e);
+ }
+ }
+ for (String jid : jids) {
+ msg.setTo(Jid.of(jid));
+ router.send(ClientMessage.from(msg));
+ }
+ }
+
+ public void sendJuickComment(com.juick.Message jmsg, List<User> users) {
+ String replyQuote;
+ String replyTo;
+
+ 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 + " ";
+ String attachmentUrl = MessageUtils.attachmentUrl(fullReply);
+ if (StringUtils.isNotEmpty(attachmentUrl)) {
+ txt += attachmentUrl + "\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(jid);
+ 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));
+ router.send(ClientMessage.from(msg));
+ }
+ }
+ }
+
+ @Override
+ public void processMessageEvent(MessageEvent event) {
+ com.juick.Message msg = event.getMessage();
+ List<User> subscribers = event.getUsers();
+ if (MessageUtils.isPM(msg)) {
+ userService.getJIDsbyUID(msg.getTo().getUid())
+ .forEach(userJid -> {
+ Message mm = new Message();
+ mm.setTo(Jid.of(userJid));
+ mm.setType(Message.Type.CHAT);
+ boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid);
+ if (inroster) {
+ mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick"));
+ mm.setBody(msg.getText());
+ } else {
+ mm.setFrom(jid);
+ mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText());
+ }
+ router.send(ClientMessage.from(mm));
+ });
+ } else if (MessageUtils.isReply(msg)) {
+ sendJuickComment(msg, subscribers);
+ }
+ else {
+ sendJuickMessage(msg, subscribers);
+ }
+ }
+
+ private ClientMessage makeReply(Jid jidTo, String txt) {
+ Message reply = new Message();
+ reply.setFrom(jid);
+ reply.setTo(jidTo);
+ reply.setType(Message.Type.CHAT);
+ reply.setBody(txt);
+ return ClientMessage.from(reply);
+ }
+
+ @Override
+ public void processSubscribeEvent(SubscribeEvent subscribeEvent) {
+
+ }
+
+ @Override
+ public void processLikeEvent(LikeEvent likeEvent) {
+ List<User> users = likeEvent.getSubscribers();
+ com.juick.Message jmsg = likeEvent.getMessage();
+ User liker = likeEvent.getUser();
+
+ String txt = "Recommended by @" + liker.getName() + ":\n";
+ txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n";
+ String attachmentUrl = MessageUtils.attachmentUrl(jmsg);
+ if (StringUtils.isNotEmpty(attachmentUrl)) {
+ txt += attachmentUrl + "\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(jid);
+ msg.setBody(txt);
+ msg.setType(Message.Type.CHAT);
+ msg.setThread("juick-" + jmsg.getMid());
+ msg.addExtension(jmsg);
+ msg.addExtension(nick);
+ if (StringUtils.isNotEmpty(attachmentUrl)) {
+ try {
+ OobX oob = new OobX(new URI(attachmentUrl));
+ 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));
+ router.send(ClientMessage.from(msg));
+ }
+ }
+ }
+
+ @Override
+ public void ProcessPingEvent(PingEvent pingEvent) {
+ userService.getJIDsbyUID(pingEvent.getPinger().getUid())
+ .forEach(userJid -> {
+ Presence p = new Presence(Jid.of(userJid));
+ p.setFrom(jid);
+ p.setPriority((byte) 10);
+ router.send(ClientPresence.from(p));
+ });
+ }
+
+ private void 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);
+ router.send(ClientPresence.from(reply));
+ } 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");
+ }
+ router.send(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));
+ router.send(ClientPresence.from(reply));
+ }
+ } 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);
+ router.send(ClientPresence.from(reply));
+
+ reply.setFrom(reply.getFrom().withResource(jid.getResource()));
+ reply.setPriority((byte) 10);
+ reply.setType(null);
+ router.send(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));
+ router.send(ClientPresence.from(reply));
+ }
+ } 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);
+ router.send(ClientPresence.from(reply));
+ }
+ }
+
+ public ClientMessage incomingMessage(Message msg) {
+ ClientMessage result = null;
+ 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 null;
+ }
+ }
+ return null;
+ }
+ Jid to = msg.getTo();
+ if (to.getDomain().equals(router.getDomain().toEscapedString()) || to.equals(this.jid)) {
+ User user_from;
+ if (msg.getFrom().getDomain().equals("uid.juick.com")) {
+ user_from = userService.getUserByUID(NumberUtils.toInt(msg.getFrom().getLocal(),
+ 0)).orElse(new User());
+ } else {
+ user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString());
+ }
+ if ((user_from == null || user_from.getUid() == 0) && !msg.getFrom().equals(jid)) {
+ String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString());
+ return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: 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.");
+ }
+
+ com.juick.Message jmsg = msg.getExtension(com.juick.Message.class);
+ if (jmsg != null) {
+ if (to.getLocal().equals("pm")) {
+ applicationEventPublisher.publishEvent(new MessageEvent(this, jmsg, Collections.singletonList(jmsg.getTo())));
+ } else {
+ if (MessageUtils.isReply(jmsg)) {
+ // to get quote and attachment
+ com.juick.Message original = messagesService.getMessage(jmsg.getMid());
+ com.juick.Message reply = messagesService.getReply(jmsg.getMid(), jmsg.getRid());
+ applicationEventPublisher.publishEvent(new MessageEvent(this, reply,
+ subscriptionService.getUsersSubscribedToComments(original, reply)));
+ } else if (!MessageUtils.isPM(jmsg)) {
+ applicationEventPublisher.publishEvent(new MessageEvent(this,
+ messagesService.getMessage(jmsg.getMid()), subscriptionService.getSubscribedUsers(jmsg.getUser().getUid(), jmsg.getMid())));
+ }
+ }
+ } else {
+ URI attachment = URI.create(StringUtils.EMPTY);
+ OobX oobX = msg.getExtension(OobX.class);
+ if (oobX != null) {
+ attachment = oobX.getUri();
+ }
+ try {
+ if (msg.getTo().asBareJid().equals(jid.asBareJid())) {
+ return incomingMessageJuick(user_from, msg.getFrom(), StringUtils.defaultString(msg.getBody()), attachment);
+ } else {
+ // PM
+ result = incomingMessageJuick(user_from, msg.getFrom(),
+ String.format("@%s %s", msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody())), attachment);
+ }
+ } catch (Exception e1) {
+ logger.warn("message exception", e1);
+ }
+ }
+ } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain())
+ || to.getDomain().endsWith("." + jid.getDomain()))) {
+ if (logger.isInfoEnabled()) {
+ try {
+ logger.info("unhandled message: {}", stanzaToString(msg));
+ } catch (JAXBException | XMLStreamException ex) {
+ logger.error("JAXB exception", ex);
+ }
+ }
+ } else {
+ return ClientMessage.from(msg);
+ }
+ return result;
+ }
+ private ClientMessage incomingMessageJuick(User user_from, Jid from, String command, @Nonnull URI attachment) {
+ if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) {
+ return null;
+ }
+
+ int commandlen = command.length();
+
+ // COMPATIBILITY
+ if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) {
+ command = command.substring(3);
+ }
+
+ try {
+ CommandResult result = commandsManager.processCommand(user_from, command.trim(), attachment);
+ if (StringUtils.isNotBlank(result.getText())) {
+ return makeReply(from, result.getText());
+ }
+ } catch (Exception e) {
+ logger.warn("xmpp command exception", e);
+ return makeReply(from, "Error processing command");
+ }
+ return null;
+ }
+
+ @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;
+ ClientMessage result = incomingMessage(msg);
+ if (result != null) {
+ router.send(result);
+ }
+ } else if (xmlValue instanceof IQ) {
+ IQ iq = (IQ) xmlValue;
+ router.send(iq);
+ }
+ }
+
+ private 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));
+ router.send(ClientPresence.from(presence));
+ } catch (IllegalArgumentException ex) {
+ logger.warn("Invalid jid: {}", j, ex);
+ }
+ });
+ }
+
+ @PreDestroy
+ public void close() throws Exception {
+ broadcastPresence(Presence.Type.UNAVAILABLE);
+ if (router != null) {
+ router.close();
+ }
+ }
+
+ public ExternalComponent getRouter() {
+ return router;
+ }
+}
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..bf8ed228
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/XMPPServer.java
@@ -0,0 +1,413 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server;
+
+import com.juick.server.xmpp.s2s.*;
+import com.juick.service.UserService;
+import com.juick.xmpp.extensions.StreamError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.xmlpull.v1.XmlPullParserException;
+import rocks.xmpp.addr.Jid;
+import rocks.xmpp.core.stanza.model.Stanza;
+
+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 java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+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("com.juick.server.xmpp");
+
+ private static final int TIMEOUT_MINUTES = 15;
+
+ @Inject
+ public ExecutorService service;
+ @Value("${hostname:localhost}")
+ private Jid jid;
+ @Value("${s2s_port:5269}")
+ private int s2sPort;
+ @Value("${keystore:juick.p12}")
+ public String keystore;
+ @Value("${keystore_password:secret}")
+ public String keystorePassword;
+ @Value("${broken_ssl_hosts:}")
+ public String[] brokenSSLhosts;
+ @Value("${banned_hosts:}")
+ public String[] bannedHosts;
+
+ private final List<ConnectionIn> inConnections = new CopyOnWriteArrayList<>();
+ private final Map<ConnectionOut, Optional<Socket>> outConnections = new ConcurrentHashMap<>();
+ private final List<CacheEntry> outCache = new CopyOnWriteArrayList<>();
+ private final List<StanzaListener> 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 boolean tlsConfigured = false;
+
+
+ private ServerSocket listener;
+
+ @Inject
+ private BasicXmppSession session;
+ @Inject
+ private UserService userService;
+
+ @PostConstruct
+ public void init() throws KeyStoreException {
+ closeFlag.set(false);
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ 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());
+ tlsConfigured = true;
+ } 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 != null && !listener.isClosed()) {
+ listener.close();
+ }
+ outConnections.forEach((c, s) -> {
+ c.logoff();
+ outConnections.remove(c);
+ });
+ inConnections.forEach(c -> {
+ c.closeConnection();
+ inConnections.remove(c);
+ });
+ 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> 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<ConnectionOut> getConnectionOut(Jid hostname, boolean needReady) {
+ return outConnections.keySet().stream().filter(c -> c.to != null &&
+ c.to.equals(hostname) && (!needReady || c.streamReady)).findFirst();
+ }
+
+ public Optional<ConnectionIn> getConnectionIn(String streamID) {
+ return inConnections.stream().filter(c -> c.streamID != null && c.streamID.equals(streamID)).findFirst();
+ }
+
+ 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.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<ConnectionOut> 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) {
+ logger.info("S2S: {}", xmlValue);
+ Stanza stanza = parse(xmlValue);
+ stanzaListeners.forEach(l -> l.stanzaReceived(stanza));
+ }
+
+ public BasicXmppSession getSession() {
+ return session;
+ }
+
+ public List<ConnectionIn> getInConnections() {
+ return inConnections;
+ }
+
+ public Map<ConnectionOut, Optional<Socket>> getOutConnections() {
+ return outConnections;
+ }
+
+ @Override
+ public boolean isTlsAvailable() {
+ return tlsConfigured;
+ }
+
+ @Override
+ public void starttls(ConnectionIn connection) {
+ logger.debug("stream {} securing", connection.streamID);
+ connection.sendStanza("<proceed xmlns=\"" + Connection.NS_TLS + "\" />");
+ 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.debug("stream {} secured", connection.streamID);
+ connection.restartParser();
+ } catch (XmlPullParserException | IOException sex) {
+ logger.warn("stream {} ssl error {}", connection.streamID, sex);
+ connection.sendStanza("<failed xmlns\"" + Connection.NS_TLS + "\" />");
+ 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.debug("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("<failed xmlns\"" + Connection.NS_TLS + "\" />");
+ 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.debug("stream to {} {} ready", connection.to, connection.getStreamID());
+ String cache = getFromCache(connection.to);
+ if (cache != null) {
+ logger.debug("stream to {} {} sending cache", connection.to, connection.getStreamID());
+ connection.send(cache);
+ }
+ }
+
+ @Override
+ public boolean securing(ConnectionOut connection) {
+ return tlsConfigured && !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;
+ }
+ @Scheduled(fixedDelay = 10000)
+ public void cleanUp() {
+ Instant now = Instant.now();
+ outConnections.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();
+ outConnections.remove(c);
+ });
+
+ inConnections.stream().filter(c -> Duration.between(now, c.updated).toMinutes() > TIMEOUT_MINUTES)
+ .forEach(c -> {
+ logger.info("closing idle incoming connection from {}", c.from);
+ c.closeConnection();
+ inConnections.remove(c);
+ });
+ }
+ @PreDestroy
+ public void preDestroy() {
+ closeFlag.set(true);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/api/Messages.java b/juick-server/src/main/java/com/juick/server/api/Messages.java
index c6600e2b..7eb86284 100644
--- a/juick-server/src/main/java/com/juick/server/api/Messages.java
+++ b/juick-server/src/main/java/com/juick/server/api/Messages.java
@@ -18,11 +18,9 @@
package com.juick.server.api;
import com.juick.Message;
-import com.juick.Status;
import com.juick.Tag;
import com.juick.User;
import com.juick.server.util.HttpBadRequestException;
-import com.juick.server.util.HttpForbiddenException;
import com.juick.server.util.UserUtils;
import com.juick.service.MessagesService;
import com.juick.service.TagService;
@@ -33,7 +31,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
-import springfox.documentation.annotations.ApiIgnore;
import javax.inject.Inject;
import java.io.IOException;
@@ -167,20 +164,4 @@ public class Messages {
}
throw new HttpBadRequestException();
}
- @ApiIgnore
- @RequestMapping("/messages/set_privacy")
- @ResponseBody
- public ResponseEntity<Status> doSetPrivacy(
- @RequestParam(defaultValue = "0") int mid) {
- User visitor = UserUtils.getCurrentUser();
- int vuid = visitor.getUid();
- if (vuid == 0) {
- throw new HttpForbiddenException();
- }
- com.juick.User user = messagesService.getMessageAuthor(mid);
- if (user != null && user.getUid() == vuid && messagesService.setMessagePrivacy(mid)) {
- return ResponseEntity.ok(Status.OK);
- }
- throw new HttpForbiddenException();
- }
}
diff --git a/juick-server/src/main/java/com/juick/server/api/PM.java b/juick-server/src/main/java/com/juick/server/api/PM.java
index a09ecc2d..3649bb5e 100644
--- a/juick-server/src/main/java/com/juick/server/api/PM.java
+++ b/juick-server/src/main/java/com/juick/server/api/PM.java
@@ -18,7 +18,6 @@
package com.juick.server.api;
import com.juick.User;
-import com.juick.server.ServerManager;
import com.juick.server.component.MessageEvent;
import com.juick.server.helpers.AnonymousUser;
import com.juick.server.helpers.PrivateChats;
@@ -31,8 +30,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import rocks.xmpp.addr.Jid;
-import rocks.xmpp.core.stanza.model.Message;
import javax.inject.Inject;
import java.util.Collections;
@@ -48,8 +45,6 @@ public class PM {
@Inject
private PMQueriesService pmQueriesService;
@Inject
- private ServerManager serverManager;
- @Inject
private ApplicationEventPublisher applicationEventPublisher;
@RequestMapping(value = "/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
diff --git a/juick-server/src/main/java/com/juick/server/api/Post.java b/juick-server/src/main/java/com/juick/server/api/Post.java
deleted file mode 100644
index 486e9c0f..00000000
--- a/juick-server/src/main/java/com/juick/server/api/Post.java
+++ /dev/null
@@ -1,288 +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 <http://www.gnu.org/licenses/>.
- */
-
-package com.juick.server.api;
-
-import com.juick.Reaction;
-import com.juick.Status;
-import com.juick.User;
-import com.juick.server.CommandsManager;
-import com.juick.server.EmailManager;
-import com.juick.server.XMPPConnection;
-import com.juick.server.helpers.CommandResult;
-import com.juick.server.util.*;
-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.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.mail.util.MimeMessageParser;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
-import springfox.documentation.annotations.ApiIgnore;
-
-import javax.inject.Inject;
-import javax.mail.Session;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeMessage;
-import javax.validation.constraints.NotNull;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Paths;
-import java.util.*;
-
-/**
- * Created by vt on 24/11/2016.
- */
-@RestController
-public class Post {
- private static Logger logger = LoggerFactory.getLogger(Post.class);
-
- @Inject
- private UserService userService;
- @Inject
- private XMPPConnection xmppConnection;
- @Inject
- private MessagesService messagesService;
- @Inject
- private SubscriptionService subscriptionService;
- @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
- private String tmpDir;
- @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
- private String imgDir;
- @Value("${api_user:juick}")
- private String serviceUser;
- @Inject
- CommandsManager commandsManager;
-
- @RequestMapping(value = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
- @ResponseStatus(value = HttpStatus.OK)
- public void doPostMessage(
- @RequestParam String body,
- @RequestParam(required = false) String img,
- @RequestParam(required = false) MultipartFile attach) throws Exception {
- User visitor = UserUtils.getCurrentUser();
-
- if (visitor.isAnonymous())
- throw new HttpForbiddenException();
-
- if (body == null || body.length() < 1 || body.length() > 4096) {
- throw new HttpBadRequestException();
- }
- body = body.replace("\r", StringUtils.EMPTY);
-
- URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
-
- if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) {
- try {
- URL imgUrl = new URL(img);
- attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir);
- } catch (Exception e) {
- logger.error("DOWNLOAD ERROR", e);
- throw new HttpBadRequestException();
- }
- }
- commandsManager.processCommand(visitor, body, attachmentFName);
- }
- @PostMapping("/upload")
- public String doUploadFile(@RequestParam(required = true) MultipartFile attach) {
- return HttpUtils.receiveMultiPartFile(attach, tmpDir).toString();
- }
-
- @RequestMapping(value = "/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
- public com.juick.Message doPostComment(
- @RequestParam(defaultValue = "0") int mid,
- @RequestParam(defaultValue = "0") int rid,
- @RequestParam String body,
- @RequestParam(required = false) String img,
- @RequestParam(required = false) MultipartFile attach)
- throws Exception {
- User visitor = UserUtils.getCurrentUser();
- int vuid = visitor.getUid();
- if (vuid == 0) {
- throw new HttpForbiddenException();
- }
- if (mid == 0) {
- throw new HttpBadRequestException();
- }
- com.juick.Message msg = messagesService.getMessage(mid);
- if (msg == null) {
- throw new HttpNotFoundException();
- }
-
- com.juick.Message reply = null;
- if (rid > 0) {
- reply = messagesService.getReply(mid, rid);
- if (reply == null) {
- throw new HttpNotFoundException();
- }
- }
-
- if (body == null || body.length() < 1 || body.length() > 4096) {
- throw new HttpBadRequestException();
- }
- body = body.replace("\r", StringUtils.EMPTY);
-
- if ((msg.ReadOnly && msg.getUser().getUid() != vuid) || userService.isInBLAny(msg.getUser().getUid(), vuid)
- || (reply != null && userService.isInBLAny(reply.getUser().getUid(), vuid))) {
- throw new HttpForbiddenException();
- }
-
- URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
-
- if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) {
- try {
- attachmentFName = HttpUtils.downloadImage(new URL(img), tmpDir);
- } catch (Exception e) {
- logger.error("DOWNLOAD ERROR", e);
- throw new HttpBadRequestException();
- }
- }
-
- return commandsManager.processCommand(visitor, String.format("#%d/%d %s", mid, rid, body), attachmentFName).getNewMessage().get();
- }
-
- Session session = Session.getDefaultInstance(new Properties());
-
- @ApiIgnore
- @PostMapping("/mail")
- @ResponseStatus(value = HttpStatus.OK)
- public void processMail(InputStream data) throws Exception {
- if (UserUtils.getCurrentUser().getName().equals(serviceUser)) {
- MimeMessage msg = new MimeMessage(session, data);
- logger.info("got msg {}", msg.toString());
- String from = msg.getFrom() == null || msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress()
- : ((InternetAddress) msg.getFrom()[0]).getAddress();
-
- User visitor = userService.getUserByEmail(from);
- if (!visitor.isAnonymous()) {
- MimeMessageParser parser = new MimeMessageParser(msg);
- parser.parse();
- final String[] body = {parser.getPlainContent()};
- if (body[0] == null) {
- parser.getAttachmentList().stream()
- .filter(a -> a.getContentType().equals("text/plain")).findFirst()
- .ifPresent(a -> {
- try {
- body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8);
- logger.info("got text: {}", body[0]);
- } catch (IOException e) {
- logger.info("attachment error: {}", e);
- }
- });
- }
- final String[] attachmentFName = new String[1];
- parser.getAttachmentList().stream().filter(a ->
- a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png"))
- .findFirst().ifPresent(a -> {
- logger.info("got attachment: {}", a.getContentType());
- String attachmentType;
- if (a.getContentType().equals("image/jpeg")) {
- attachmentType = "jpg";
- } else {
- attachmentType = "png";
- }
- attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType;
- try {
- logger.info("got inputstream: {}", a.getInputStream());
- FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString());
- IOUtils.copy(a.getInputStream(), fos);
- fos.close();
- } catch (IOException e) {
- logger.info("attachment error: {}", e);
- }
- });
- String[] inReplyToHeaders = msg.getHeader("In-Reply-To");
- if (inReplyToHeaders != null && inReplyToHeaders.length > 0) {
- Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN);
- int mid = Integer.parseInt(inReplyToScanner.next());
- int rid = Integer.parseInt(inReplyToScanner.next());
- logger.info("Message is reply to #{}/{}", mid, rid);
- body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0])
- : String.format("#%d %s", mid, body[0]);
- }
- URI attachmentUri = StringUtils.isNotEmpty(attachmentFName[0]) ? URI.create(String.format("juick://%s", attachmentFName[0]))
- : URI.create(StringUtils.EMPTY);
- commandsManager.processCommand(visitor, body[0], attachmentUri);
- } else {
- logger.info("not registered: {}", from);
- }
- } else {
- throw new HttpForbiddenException();
- }
- }
-
- @PostMapping("/like")
- @ResponseStatus(value = HttpStatus.OK)
- public Status doPostRecomm(@RequestParam Integer mid) throws Exception {
- com.juick.User visitor = UserUtils.getCurrentUser();
- if (visitor.getUid() == 0) {
- throw new HttpForbiddenException();
- }
- com.juick.Message msg = messagesService.getMessage(mid);
- if (msg == null) {
- throw new HttpNotFoundException();
- }
- if (msg.getUser().getUid() == visitor.getUid()) {
- throw new HttpForbiddenException();
- }
- CommandResult status = commandsManager.processCommand(visitor, String.format("! #%d", mid),
- URI.create(StringUtils.EMPTY));
- return Status.getStatus(status.getText());
- }
-
- @GetMapping("/reactions")
- @ResponseStatus(value = HttpStatus.OK)
- public List<Reaction> reactionsList() {
- return messagesService.listReactions();
- }
-
- @PostMapping("/react")
- @ResponseStatus(value = HttpStatus.OK)
- public Status doPostReact(@RequestParam Integer mid,@RequestParam @NotNull int reactionId,
- @RequestParam (required = false, defaultValue = "1") int count) {
-
- logger.info("got reaction with type: {}", reactionId);
- com.juick.User visitor = UserUtils.getCurrentUser();
- if (visitor.getUid() == 0) {
- throw new HttpForbiddenException();
- }
- com.juick.Message msg = messagesService.getMessage(mid);
- if (msg == null) {
- throw new HttpNotFoundException();
- }
- if (msg.getUser().getUid() == visitor.getUid()) {
- throw new HttpForbiddenException();
- }
- MessagesService.RecommendStatus recommendStatus = MessagesService.RecommendStatus.Error;
- for (int i = 0; i < count; i++)
- recommendStatus = messagesService.likeMessage(mid, visitor.getUid(),
- reactionId);
-
- return recommendStatus == MessagesService.RecommendStatus.Error ? Status.ERROR :Status.OK;
- }
-}
diff --git a/juick-server/src/main/java/com/juick/server/api/Service.java b/juick-server/src/main/java/com/juick/server/api/Service.java
new file mode 100644
index 00000000..f67f6986
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/api/Service.java
@@ -0,0 +1,117 @@
+package com.juick.server.api;
+
+import com.juick.User;
+import com.juick.server.CommandsManager;
+import com.juick.server.EmailManager;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.UserService;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.mail.util.MimeMessageParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.inject.Inject;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.util.Properties;
+import java.util.Scanner;
+import java.util.UUID;
+
+@Controller
+public class Service {
+ private static Logger logger = LoggerFactory.getLogger(Post.class);
+ @Inject
+ private UserService userService;
+
+ @Inject
+ CommandsManager commandsManager;
+ @Value("${api_user:juick}")
+ private String serviceUser;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ Session session = Session.getDefaultInstance(new Properties());
+
+ @ApiIgnore
+ @PostMapping("/mail")
+ @ResponseStatus(value = HttpStatus.OK)
+ public void processMail(InputStream data) throws Exception {
+ if (UserUtils.getCurrentUser().getName().equals(serviceUser)) {
+ MimeMessage msg = new MimeMessage(session, data);
+ logger.info("got msg {}", msg.toString());
+ String from = msg.getFrom() == null || msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress()
+ : ((InternetAddress) msg.getFrom()[0]).getAddress();
+
+ User visitor = userService.getUserByEmail(from);
+ if (!visitor.isAnonymous()) {
+ MimeMessageParser parser = new MimeMessageParser(msg);
+ parser.parse();
+ final String[] body = {parser.getPlainContent()};
+ if (body[0] == null) {
+ parser.getAttachmentList().stream()
+ .filter(a -> a.getContentType().equals("text/plain")).findFirst()
+ .ifPresent(a -> {
+ try {
+ body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8);
+ logger.info("got text: {}", body[0]);
+ } catch (IOException e) {
+ logger.info("attachment error: {}", e);
+ }
+ });
+ }
+ final String[] attachmentFName = new String[1];
+ parser.getAttachmentList().stream().filter(a ->
+ a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png"))
+ .findFirst().ifPresent(a -> {
+ logger.info("got attachment: {}", a.getContentType());
+ String attachmentType;
+ if (a.getContentType().equals("image/jpeg")) {
+ attachmentType = "jpg";
+ } else {
+ attachmentType = "png";
+ }
+ attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType;
+ try {
+ logger.info("got inputstream: {}", a.getInputStream());
+ FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString());
+ IOUtils.copy(a.getInputStream(), fos);
+ fos.close();
+ } catch (IOException e) {
+ logger.info("attachment error: {}", e);
+ }
+ });
+ String[] inReplyToHeaders = msg.getHeader("In-Reply-To");
+ if (inReplyToHeaders != null && inReplyToHeaders.length > 0) {
+ Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN);
+ int mid = Integer.parseInt(inReplyToScanner.next());
+ int rid = Integer.parseInt(inReplyToScanner.next());
+ logger.info("Message is reply to #{}/{}", mid, rid);
+ body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0])
+ : String.format("#%d %s", mid, body[0]);
+ }
+ URI attachmentUri = StringUtils.isNotEmpty(attachmentFName[0]) ? URI.create(String.format("juick://%s", attachmentFName[0]))
+ : URI.create(StringUtils.EMPTY);
+ commandsManager.processCommand(visitor, body[0], attachmentUri);
+ } else {
+ logger.info("not registered: {}", from);
+ }
+ } else {
+ throw new HttpForbiddenException();
+ }
+ }
+}
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 973c31fd..91f5446a 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
@@ -21,7 +21,13 @@ import com.google.common.base.Predicates;
import com.juick.server.WebsocketManager;
import com.juick.server.api.rss.MessagesView;
import com.juick.server.api.rss.RepliesView;
+import com.juick.server.xmpp.JidConverter;
+import com.juick.server.xmpp.iq.MessageQuery;
+import com.juick.server.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.WebMvcConfigurer;
@@ -31,6 +37,9 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket;
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 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;
@@ -88,4 +97,20 @@ public class ApiAppConfiguration implements WebMvcConfigurer, WebSocketConfigure
AbstractRssFeedView repliesView() {
return new RepliesView();
}
+ @Value("${hostname:localhost}")
+ private String hostname;
+ @Bean
+ public BasicXmppSession session() {
+ XmppSessionConfiguration configuration = XmppSessionConfiguration.builder()
+ .extensions(Extension.of(com.juick.Message.class), Extension.of(MessageQuery.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-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java
deleted file mode 100644
index a065f79e..00000000
--- a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java
+++ /dev/null
@@ -1,124 +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 <http://www.gnu.org/licenses/>.
- */
-
-package com.juick.server.configuration;
-
-import com.juick.service.UserService;
-import com.juick.service.security.JuickUserDetailsService;
-import com.juick.service.security.NotAuthorizedAuthenticationEntryPoint;
-import com.juick.service.security.deprecated.RequestParamHashRememberMeServices;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
-import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.builders.WebSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.web.authentication.RememberMeServices;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.CorsConfigurationSource;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-
-import javax.inject.Inject;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Created by aalexeev on 11/21/16.
- */
-@Configuration
-@EnableWebSecurity
-public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
- @Value("${auth_remember_me_key:secret}")
- private String rememberMeKey;
- @Inject
- private UserService userService;
-
- ApiSecurityConfig() {
- super(true);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests()
- .antMatchers(HttpMethod.OPTIONS).permitAll()
- .antMatchers("/", "/messages", "/users", "/thread", "/tags", "/tlgmbtwbhk", "/fbwbhk",
- "/skypebotendpoint").permitAll()
- .anyRequest().hasRole("USER")
- .and().httpBasic().authenticationEntryPoint(getJuickAuthenticationEntryPoint())
- .and().anonymous()
- .and().cors().configurationSource(corsConfigurationSource())
- .and().servletApi()
- .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and().exceptionHandling().authenticationEntryPoint(getJuickAuthenticationEntryPoint())
- .and()
- .rememberMe()
- .alwaysRemember(true)
- .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30))
- .rememberMeServices(rememberMeServices())
- .key(rememberMeKey)
- .and().authenticationProvider(authenticationProvider())
- .headers().defaultsDisabled().cacheControl();
- }
-
- @Bean
- public DaoAuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
-
- authenticationProvider.setUserDetailsService(userDetailsService());
-
- return authenticationProvider;
- }
-
- @Bean
- public JuickUserDetailsService userDetailsService() {
- return new JuickUserDetailsService(userService);
- }
-
- @Bean
- public RememberMeServices rememberMeServices() throws Exception {
- return new RequestParamHashRememberMeServices(rememberMeKey, userService);
- }
-
- @Bean
- public NotAuthorizedAuthenticationEntryPoint getJuickAuthenticationEntryPoint() {
- return new NotAuthorizedAuthenticationEntryPoint();
- }
-
- @Bean
- public CorsConfigurationSource corsConfigurationSource() {
- CorsConfiguration configuration = new CorsConfiguration();
-
- configuration.setAllowedOrigins(Collections.singletonList("*"));
- configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE"));
- configuration.setAllowedHeaders(Collections.singletonList("*"));
-
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- source.registerCorsConfiguration("/**", configuration);
-
- return source;
- }
- @Override
- public void configure(WebSecurity web) throws Exception {
- web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**",
- "/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**", "/rss/**");
- }
-}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/JidConverter.java b/juick-server/src/main/java/com/juick/server/xmpp/JidConverter.java
new file mode 100644
index 00000000..e9a9707e
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/JidConverter.java
@@ -0,0 +1,13 @@
+package com.juick.server.xmpp;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.lang.Nullable;
+import rocks.xmpp.addr.Jid;
+
+public class JidConverter implements Converter<String, Jid> {
+ @Nullable
+ @Override
+ public Jid convert(String jidStr) {
+ return Jid.of(jidStr);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java b/juick-server/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java
new file mode 100644
index 00000000..7978ceb3
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.xmpp.helpers;
+
+import com.juick.server.xmpp.s2s.ConnectionIn;
+import com.juick.server.xmpp.s2s.ConnectionOut;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by vitalyster on 16.02.2017.
+ */
+public class XMPPStatus {
+ private List<ConnectionIn> inbound;
+ private Set<ConnectionOut> outbound;
+
+ public List<ConnectionIn> getInbound() {
+ return inbound;
+ }
+
+ public void setInbound(List<ConnectionIn> inbound) {
+ this.inbound = inbound;
+ }
+
+ public Set<ConnectionOut> getOutbound() {
+ return outbound;
+ }
+
+ public void setOutbound(Set<ConnectionOut> outbound) {
+ this.outbound = outbound;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java b/juick-server/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java
new file mode 100644
index 00000000..7500cbf8
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java
@@ -0,0 +1,10 @@
+package com.juick.server.xmpp.iq;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "query")
+public class MessageQuery {
+ private MessageQuery() {
+
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/iq/package-info.java b/juick-server/src/main/java/com/juick/server/xmpp/iq/package-info.java
new file mode 100644
index 00000000..dada8289
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/iq/package-info.java
@@ -0,0 +1,8 @@
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlSchema(namespace = "http://juick.com/query#messages", elementFormDefault = XmlNsForm.QUALIFIED)
+package com.juick.server.xmpp.iq;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlNsForm;
+import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/Stream.java b/juick-server/src/main/java/com/juick/server/xmpp/router/Stream.java
new file mode 100644
index 00000000..7532443c
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/Stream.java
@@ -0,0 +1,184 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.juick.server.xmpp.router;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import rocks.xmpp.addr.Jid;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.UUID;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+public abstract class Stream {
+
+ public boolean isLoggedIn() {
+ return loggedIn;
+ }
+
+ public void setLoggedIn(boolean loggedIn) {
+ this.loggedIn = loggedIn;
+ }
+
+ Jid from;
+ public Jid to;
+ private InputStream is;
+ private OutputStream os;
+ private XmlPullParserFactory factory;
+ protected XmlPullParser parser;
+ private OutputStreamWriter writer;
+ StreamHandler streamHandler;
+ private boolean loggedIn;
+ private Instant created;
+ private Instant updated;
+ String streamId;
+ private boolean secured;
+
+ public Stream(final Jid from, final Jid to, final InputStream is, final OutputStream os) throws XmlPullParserException {
+ this.from = from;
+ this.to = to;
+ this.is = is;
+ this.os = os;
+ factory = XmlPullParserFactory.newInstance();
+ created = updated = Instant.now();
+ streamId = UUID.randomUUID().toString();
+ }
+
+ void restartStream() throws XmlPullParserException {
+ parser = factory.newPullParser();
+ parser.setInput(new InputStreamReader(is, StandardCharsets.UTF_8));
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ writer = new OutputStreamWriter(os, StandardCharsets.UTF_8);
+ }
+
+ public void connect() {
+ try {
+ restartStream();
+ handshake();
+ parse();
+ } catch (XmlPullParserException e) {
+ StreamError invalidXmlError = new StreamError("invalid-xml");
+ send(invalidXmlError.toString());
+ connectionFailed(new Exception(invalidXmlError.getCondition()));
+ } catch (IOException e) {
+ connectionFailed(e);
+ }
+ }
+
+ public void setHandler(final StreamHandler streamHandler) {
+ this.streamHandler = streamHandler;
+ }
+
+ public abstract void handshake() throws XmlPullParserException, IOException;
+
+ public void logoff() {
+ setLoggedIn(false);
+ try {
+ writer.flush();
+ writer.close();
+ //TODO close parser
+ } catch (final Exception e) {
+ connectionFailed(e);
+ }
+ }
+
+ public void send(final String str) {
+ try {
+ updated = Instant.now();
+ writer.write(str);
+ writer.flush();
+ } catch (final Exception e) {
+ connectionFailed(e);
+ }
+ }
+
+ private void parse() throws IOException, XmlPullParserException {
+ while (parser.next() != XmlPullParser.END_DOCUMENT) {
+ if (parser.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE) {
+ setUpdated();
+ }
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ setUpdated();
+ final String tag = parser.getName();
+ switch (tag) {
+ case "message":
+ case "presence":
+ case "iq":
+ streamHandler.stanzaReceived(XmlUtils.parseToString(parser, false));
+ break;
+ case "error":
+ StreamError error = StreamError.parse(parser);
+ connectionFailed(new Exception(error.getCondition()));
+ return;
+ default:
+ XmlUtils.skip(parser);
+ break;
+ }
+ }
+ }
+
+ /**
+ * This method is used to be called on a parser or a connection error.
+ * It tries to close the XML-Reader and XML-Writer one last time.
+ */
+ private void connectionFailed(final Exception ex) {
+ if (isLoggedIn()) {
+ try {
+ writer.close();
+ //TODO close parser
+ } catch (Exception e) {
+ }
+ }
+ streamHandler.fail(ex);
+ }
+
+ public Instant getCreated() {
+ return created;
+ }
+
+ public Instant getUpdated() {
+ return updated;
+ }
+ public String getStreamId() {
+ return streamId;
+ }
+
+ public boolean isSecured() {
+ return secured;
+ }
+
+ public void setSecured(boolean secured) {
+ this.secured = secured;
+ }
+
+ public void setUpdated() {
+ this.updated = Instant.now();
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java
new file mode 100644
index 00000000..5e2f6f82
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java
@@ -0,0 +1,58 @@
+package com.juick.server.xmpp.router;
+
+import com.juick.xmpp.extensions.Handshake;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.xmlpull.v1.XmlPullParserException;
+import rocks.xmpp.addr.Jid;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+/**
+ * Created by vitalyster on 30.01.2017.
+ */
+public class StreamComponentServer extends Stream {
+
+ private String streamId, secret;
+
+ public String getStreamId() {
+ return streamId;
+ }
+
+
+ public StreamComponentServer(InputStream is, OutputStream os, String password) throws XmlPullParserException {
+ super(null, null, is, os);
+ secret = password;
+ streamId = UUID.randomUUID().toString();
+ }
+ @Override
+ public void handshake() throws XmlPullParserException, IOException {
+ parser.next();
+ if (!parser.getName().equals("stream")
+ || !parser.getNamespace(null).equals(StreamNamespaces.NS_COMPONENT_ACCEPT)
+ || !parser.getNamespace("stream").equals(StreamNamespaces.NS_STREAM)) {
+ throw new IOException("invalid stream");
+ }
+ Jid domain = Jid.of(parser.getAttributeValue(null, "to"));
+ if (streamHandler.filter(null, domain)) {
+ send(new XMPPError(XMPPError.Type.cancel, "forbidden").toString());
+ throw new IOException("invalid domain");
+ }
+ from = domain;
+ to = domain;
+ send(String.format("<stream:stream xmlns:stream='%s' " +
+ "xmlns='%s' from='%s' id='%s'>", StreamNamespaces.NS_STREAM, StreamNamespaces.NS_COMPONENT_ACCEPT, from.asBareJid().toEscapedString(), streamId));
+ Handshake handshake = Handshake.parse(parser);
+ boolean authenticated = handshake.getValue().equals(DigestUtils.sha1Hex(streamId + secret));
+ setLoggedIn(authenticated);
+ if (!authenticated) {
+ send(new XMPPError(XMPPError.Type.cancel, "not-authorized").toString());
+ streamHandler.fail(new IOException("stream:stream, failed authentication"));
+ return;
+ }
+ send(new Handshake().toString());
+ streamHandler.ready();
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/StreamError.java b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamError.java
new file mode 100644
index 00000000..7eacfc94
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamError.java
@@ -0,0 +1,44 @@
+package com.juick.server.xmpp.router;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+
+/**
+ * Created by vitalyster on 03.02.2017.
+ */
+public class StreamError {
+
+ private String condition;
+
+ public StreamError() {}
+
+ public StreamError(String condition) {
+ this.condition = condition;
+ }
+
+ public static StreamError parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ StreamError streamError = new StreamError();
+ while (parser.next() == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ final String xmlns = parser.getNamespace();
+ if (xmlns.equals(StreamNamespaces.NS_XMPP_STREAMS)) {
+ streamError.condition = tag;
+ } else {
+ XmlUtils.skip(parser);
+ }
+ }
+ return streamError;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<stream:error><%s xmlns='%s'/></stream:error>", condition, StreamNamespaces.NS_XMPP_STREAMS);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/StreamHandler.java b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamHandler.java
new file mode 100644
index 00000000..43836c2d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamHandler.java
@@ -0,0 +1,13 @@
+package com.juick.server.xmpp.router;
+
+import rocks.xmpp.addr.Jid;
+
+/**
+ * Created by vitalyster on 01.02.2017.
+ */
+public interface StreamHandler {
+ void ready();
+ void fail(final Exception ex);
+ boolean filter(Jid from, Jid to);
+ void stanzaReceived(String stanza);
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java
new file mode 100644
index 00000000..1b9b1965
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java
@@ -0,0 +1,10 @@
+package com.juick.server.xmpp.router;
+
+public class StreamNamespaces {
+ public static final String NS_STREAM = "http://etherx.jabber.org/streams";
+ public static final String NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls";
+ public static final String NS_DB = "jabber:server:dialback";
+ public static final String NS_SERVER = "jabber:server";
+ public static final String NS_COMPONENT_ACCEPT = "jabber:component:accept";
+ public static final String NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams";
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPError.java b/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPError.java
new file mode 100644
index 00000000..0cf9a3bc
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPError.java
@@ -0,0 +1,73 @@
+/*
+ * Juick
+ * Copyright (C) 2008-2013, ugnich
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.juick.server.xmpp.router;
+
+import org.apache.commons.text.StringEscapeUtils;
+
+/**
+ *
+ * @author ugnich
+ */
+public class XMPPError {
+
+ public static final class Type {
+
+ public static final String auth = "auth";
+ public static final String cancel = "cancel";
+ public static final String continue_ = "continue";
+ public static final String modify = "modify";
+ public static final String wait = "wait";
+ }
+ private final static String TagName = "error";
+ public String by = null;
+ private String type;
+ private String condition;
+ private String text = null;
+
+ public XMPPError(String type, String condition) {
+ this.type = type;
+ this.condition = condition;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder("<").append(TagName).append("");
+ if (by != null) {
+ str.append(" by=\"").append(StringEscapeUtils.escapeXml10(by)).append("\"");
+ }
+ if (type != null) {
+ str.append(" type=\"").append(StringEscapeUtils.escapeXml10(type)).append("\"");
+ }
+
+ if (condition != null) {
+ str.append(">");
+ str.append("<").append(StringEscapeUtils.escapeXml10(condition)).append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"");
+ if (text != null) {
+ str.append(">").append(StringEscapeUtils.escapeXml10(text)).append("</").append(StringEscapeUtils.escapeXml10(condition))
+ .append(">");
+ } else {
+ str.append("/>");
+ }
+ str.append("</").append(TagName).append(">");
+ } else {
+ str.append("/>");
+ }
+
+ return str.toString();
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java b/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java
new file mode 100644
index 00000000..6edecf05
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java
@@ -0,0 +1,189 @@
+package com.juick.server.xmpp.router;
+
+import com.juick.server.XMPPServer;
+import com.juick.server.xmpp.s2s.BasicXmppSession;
+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.IQ;
+import rocks.xmpp.core.stanza.model.Message;
+import rocks.xmpp.core.stanza.model.Presence;
+import rocks.xmpp.core.stanza.model.Stanza;
+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.util.XmppUtils;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+@Component
+public class XMPPRouter implements StreamHandler {
+ private static final Logger logger = LoggerFactory.getLogger(XMPPRouter.class);
+
+ @Inject
+ private ExecutorService service;
+
+ private final List<StreamComponentServer> connections = Collections.synchronizedList(new ArrayList<>());
+
+ private ServerSocket listener;
+
+ @Inject
+ private BasicXmppSession session;
+
+ @Value("${router_port:5347}")
+ private int routerPort;
+
+ @Inject
+ private XMPPServer xmppServer;
+
+ @PostConstruct
+ public void init() {
+ logger.info("component router initialized");
+ service.submit(() -> {
+ try {
+ listener = new ServerSocket(routerPort);
+ logger.info("component router listening on {}", routerPort);
+ while (!listener.isClosed()) {
+ if (Thread.currentThread().isInterrupted()) break;
+ Socket socket = listener.accept();
+ service.submit(() -> {
+ try {
+ StreamComponentServer client = new StreamComponentServer(socket.getInputStream(), socket.getOutputStream(), "secret");
+ addConnectionIn(client);
+ client.setHandler(this);
+ client.connect();
+ } catch (IOException e) {
+ logger.error("component error", e);
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ } catch (SocketException e) {
+ // shutdown
+ } catch (IOException e) {
+ logger.warn("io exception", e);
+ }
+ });
+ }
+
+ @PreDestroy
+ public void close() throws Exception {
+ if (!listener.isClosed()) {
+ listener.close();
+ }
+ synchronized (getConnections()) {
+ for (Iterator<StreamComponentServer> i = getConnections().iterator(); i.hasNext(); ) {
+ StreamComponentServer c = i.next();
+ c.logoff();
+ i.remove();
+ }
+ }
+ service.shutdown();
+ logger.info("XMPP router destroyed");
+ }
+
+ private void addConnectionIn(StreamComponentServer c) {
+ synchronized (getConnections()) {
+ getConnections().add(c);
+ }
+ }
+
+ private 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("XMPPRouter (out): {}", xml);
+ sendOut(s.getTo().getDomain(), xml);
+ } catch (XMLStreamException | JAXBException e1) {
+ logger.info("jaxb exception", e1);
+ }
+ }
+
+ private void sendOut(String hostname, String xml) {
+ boolean haveAnyConn = false;
+
+ StreamComponentServer connOut = null;
+ synchronized (getConnections()) {
+ for (StreamComponentServer c : getConnections()) {
+ if (c.to != null && c.to.getDomain().equals(hostname)) {
+ if (c.isLoggedIn()) {
+ connOut = c;
+ break;
+ }
+ }
+ }
+ }
+ if (connOut != null) {
+ connOut.send(xml);
+ return;
+ }
+ xmppServer.sendOut(Jid.of(hostname), xml);
+
+ }
+
+ public List<StreamComponentServer> getConnections() {
+ return connections;
+ }
+
+ private 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;
+ }
+ @Override
+ public void stanzaReceived(String stanza) {
+ Stanza input = parse(stanza);
+ if (input instanceof Message) {
+ sendOut(ServerMessage.from((Message)input));
+ } else if (input instanceof IQ) {
+ sendOut(ServerIQ.from((IQ)input));
+ } else {
+ sendOut(ServerPresence.from((Presence) input));
+ }
+ }
+
+ @Override
+ public void ready() {
+
+ }
+
+ @Override
+ public void fail(Exception e) {
+
+ }
+
+ @Override
+ public boolean filter(Jid jid, Jid jid1) {
+ return false;
+ }
+} \ No newline at end of file
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/router/XmlUtils.java b/juick-server/src/main/java/com/juick/server/xmpp/router/XmlUtils.java
new file mode 100644
index 00000000..7579489f
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/router/XmlUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.juick.server.xmpp.router;
+
+import java.io.IOException;
+
+import org.apache.commons.text.StringEscapeUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+public class XmlUtils {
+
+ public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ String tag = parser.getName();
+ while (parser.getName() != null && !(parser.next() == XmlPullParser.END_TAG && parser.getName().equals(tag))) {
+ }
+ }
+
+ public static String getTagText(XmlPullParser parser) throws XmlPullParserException, IOException {
+ String ret = "";
+ String tag = parser.getName();
+
+ if (parser.next() == XmlPullParser.TEXT) {
+ ret = parser.getText();
+ }
+
+ while (!(parser.getEventType() == XmlPullParser.END_TAG && parser.getName().equals(tag))) {
+ parser.next();
+ }
+
+ return ret;
+ }
+
+ public static String parseToString(XmlPullParser parser, boolean skipXMLNS) throws XmlPullParserException, IOException {
+ String tag = parser.getName();
+ StringBuilder ret = new StringBuilder("<").append(tag);
+
+ // skipXMLNS for xmlns="jabber:client"
+
+ String ns = parser.getNamespace();
+ if (!skipXMLNS && ns != null && !ns.isEmpty()) {
+ ret.append(" xmlns=\"").append(ns).append("\"");
+ }
+
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String attr = parser.getAttributeName(i);
+ if ((!skipXMLNS || !attr.equals("xmlns")) && !attr.contains(":")) {
+ ret.append(" ").append(attr).append("=\"").append(StringEscapeUtils.escapeXml10(parser.getAttributeValue(i))).append("\"");
+ }
+ }
+ ret.append(">");
+
+ while (!(parser.next() == XmlPullParser.END_TAG && parser.getName().equals(tag))) {
+ int event = parser.getEventType();
+ if (event == XmlPullParser.START_TAG) {
+ if (!parser.getName().contains(":")) {
+ ret.append(parseToString(parser, false));
+ } else {
+ skip(parser);
+ }
+ } else if (event == XmlPullParser.TEXT) {
+ ret.append(StringEscapeUtils.escapeXml10(parser.getText()));
+ }
+ }
+
+ ret.append("</").append(tag).append(">");
+ return ret.toString();
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java
new file mode 100644
index 00000000..647f2717
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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/server/xmpp/s2s/CacheEntry.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java
new file mode 100644
index 00000000..33e875bd
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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/server/xmpp/s2s/Connection.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/Connection.java
new file mode 100644
index 00000000..6bf61169
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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 += ">...</" + parser.getName() + ">\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.debug("closing stream {}", streamID);
+ }
+
+ try {
+ writer.write("</stream:stream>");
+ } 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/server/xmpp/s2s/ConnectionIn.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java
new file mode 100644
index 00000000..9ee81d4d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java
@@ -0,0 +1,213 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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<Jid> 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.debug("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("<db:verify from='" + vto + "' to='" + vfrom + "' id='" + vid + "' type='valid'/>");
+ logger.debug("stream from {} {} dialback verify valid", vfrom, streamID);
+ } else {
+ sendStanza("<db:verify from='" + vto + "' to='" + vfrom + "' id='" + vid + "' type='invalid'/>");
+ logger.warn("stream from {} {} dialback verify invalid", vfrom, streamID);
+ }
+ } else if (tag.equals("presence") && checkFromTo(parser)) {
+ String xml = XmlUtils.parseToString(parser, false);
+ logger.debug("stream {} presence: {}", streamID, xml);
+ xmpp.onStanzaReceived(xml);
+ } else if (tag.equals("message") && checkFromTo(parser)) {
+ updateTsRemoteData();
+ String xml = XmlUtils.parseToString(parser, false);
+ logger.debug("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.debug("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.debug("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.debug("stream {} closed (dirty)", streamID);
+ xmpp.removeConnectionIn(this);
+ closeConnection();
+ } catch (Exception e) {
+ logger.debug("stream {} error {}", streamID, e);
+ xmpp.removeConnectionIn(this);
+ closeConnection();
+ }
+ }
+
+ void updateTsRemoteData() {
+ received = Instant.now();
+ }
+
+ void sendOpenStream(String from, boolean xmppversionnew) throws IOException {
+ String openStream = "<?xml version='1.0'?><stream:stream xmlns='jabber:server' " +
+ "xmlns:stream='http://etherx.jabber.org/streams' xmlns:db='jabber:server:dialback' from='" +
+ xmpp.getJid().toEscapedString() + "' id='" + streamID + "' version='1.0'>";
+ if (xmppversionnew) {
+ openStream += "<stream:features>";
+ if (listener != null && listener.isTlsAvailable() && !isSecured() && !Arrays.asList(xmpp.brokenSSLhosts).contains(from)) {
+ openStream += "<starttls xmlns=\"" + NS_TLS + "\"><optional/></starttls>";
+ }
+ openStream += "</stream:features>";
+ }
+ sendStanza(openStream);
+ }
+
+ public void sendDialbackResult(Jid sfrom, String type) {
+ sendStanza("<db:result from='" + xmpp.getJid().toEscapedString() + "' to='" + sfrom + "' type='" + type + "'/>");
+ if (type.equals("valid")) {
+ from.add(sfrom);
+ logger.debug("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 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/server/xmpp/s2s/ConnectionListener.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java
new file mode 100644
index 00000000..fde7a0e7
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java
@@ -0,0 +1,15 @@
+package com.juick.server.xmpp.s2s;
+
+import com.juick.xmpp.extensions.StreamError;
+
+public interface ConnectionListener {
+ boolean isTlsAvailable();
+ 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/server/xmpp/s2s/ConnectionOut.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java
new file mode 100644
index 00000000..e3bd53e9
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.xmpp.s2s;
+
+import com.juick.server.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("<?xml version='1.0'?><stream:stream xmlns='jabber:server' id='" + streamID +
+ "' xmlns:stream='http://etherx.jabber.org/streams' xmlns:db='jabber:server:dialback' from='" +
+ from.toEscapedString() + "' to='" + to.toEscapedString() + "' version='1.0'>");
+ }
+
+ void processDialback() throws Exception {
+ if (checkSID != null) {
+ sendDialbackVerify(checkSID, dbKey);
+ }
+ send("<db:result from='" + from.toEscapedString() + "' to='" + to.toEscapedString() + "'>" +
+ dbKey + "</db:result>");
+ }
+
+ @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.debug("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.warn("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.debug("stream to {} {} securing", to.toEscapedString(), streamID);
+ send("<starttls xmlns=\"" + NS_TLS + "\" />");
+ } 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("<db:verify from='" + from.toEscapedString() + "' to='" + to + "' id='" + sid + "'>" +
+ key + "</db:verify>");
+ }
+ 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/server/xmpp/s2s/DNSQueries.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java
new file mode 100644
index 00000000..1367d333
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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<String, String> 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.debug("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/server/xmpp/s2s/StanzaListener.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java
new file mode 100644
index 00000000..6932298f
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.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/server/xmpp/s2s/util/DialbackUtils.java b/juick-server/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java
new file mode 100644
index 00000000..d25dbad8
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java
@@ -0,0 +1,37 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server.xmpp.s2s.util;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.codec.digest.HmacAlgorithms;
+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 new HmacUtils(HmacAlgorithms.HMAC_SHA_256, DigestUtils.sha256(secret))
+ .hmacHex(to.toEscapedString() + " " + from.toEscapedString() + " " + id);
+ }
+}