diff options
author | Vitaly Takmazov | 2018-11-08 21:38:27 +0300 |
---|---|---|
committer | Vitaly Takmazov | 2018-11-08 21:38:27 +0300 |
commit | 7aaa3f9a29c280f01c677c918932620be45cdbd7 (patch) | |
tree | 39947b2c889afd08f9c73ba54fab91159d2af258 /src/main/java/com/juick/server/xmpp | |
parent | 3ea9770d0d43fbe45449ac4531ec4b0a374d98ea (diff) |
Merge everything into single Spring Boot application
Diffstat (limited to 'src/main/java/com/juick/server/xmpp')
24 files changed, 1797 insertions, 0 deletions
diff --git a/src/main/java/com/juick/server/xmpp/JidConverter.java b/src/main/java/com/juick/server/xmpp/JidConverter.java new file mode 100644 index 00000000..e9a9707e --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java b/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java new file mode 100644 index 00000000..231696ec --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java @@ -0,0 +1,32 @@ +package com.juick.server.xmpp; + +import com.juick.server.XMPPServer; +import com.juick.server.xmpp.helpers.XMPPStatus; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import rocks.xmpp.addr.Jid; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import java.util.stream.Collectors; + +@RestController +@ConditionalOnProperty("xmppbot_jid") +public class XMPPStatusPage { + @Inject + private XMPPServer xmpp; + @ApiIgnore + @RequestMapping(method = RequestMethod.GET, value = "/api/xmpp-status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public XMPPStatus xmppStatus() { + XMPPStatus status = new XMPPStatus(); + if (xmpp != null) { + status.setInbound(xmpp.getInConnections().stream().map(c -> c.from).flatMap(j -> j.stream().map(Jid::getDomain)).collect(Collectors.toList())); + status.setOutbound(xmpp.getOutConnections().keySet().stream() + .map(c -> c.to).map(Jid::getDomain).collect(Collectors.toList())); + } + return status; + } +} diff --git a/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java b/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java new file mode 100644 index 00000000..99d89866 --- /dev/null +++ b/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<String> inbound; + private List<String> outbound; + + public List<String> getInbound() { + return inbound; + } + + public void setInbound(List<String> inbound) { + this.inbound = inbound; + } + + public List<String> getOutbound() { + return outbound; + } + + public void setOutbound(List<String> outbound) { + this.outbound = outbound; + } +} diff --git a/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java b/src/main/java/com/juick/server/xmpp/iq/MessageQuery.java new file mode 100644 index 00000000..7500cbf8 --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/iq/package-info.java b/src/main/java/com/juick/server/xmpp/iq/package-info.java new file mode 100644 index 00000000..dada8289 --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/router/Handshake.java b/src/main/java/com/juick/server/xmpp/router/Handshake.java new file mode 100644 index 00000000..0bc501dd --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/Handshake.java @@ -0,0 +1,39 @@ +package com.juick.server.xmpp.router; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Created by vitalyster on 30.01.2017. + */ +public class Handshake { + private String value; + + public static Handshake parse(XmlPullParser parser) throws IOException, XmlPullParserException { + parser.next(); + Handshake handshake = new Handshake(); + handshake.setValue(XmlUtils.getTagText(parser)); + return handshake; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder("<handshake"); + if (getValue() != null) { + str.append(">").append(getValue()).append("</handshake>"); + } else { + str.append("/>"); + } + return str.toString(); + } +} diff --git a/src/main/java/com/juick/server/xmpp/router/Stream.java b/src/main/java/com/juick/server/xmpp/router/Stream.java new file mode 100644 index 00000000..2154edf6 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/Stream.java @@ -0,0 +1,202 @@ +/* + * 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; + } + + public 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(); + } + + public 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) { + } + } + if (streamHandler != null) { + 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(); + } + + public InputStream getInputStream() { + return is; + } + + public void setInputStream(InputStream is) { + this.is = is; + } + + public OutputStream getOutputStream() { + return os; + } + + public void setOutputStream(OutputStream os) { + this.os = os; + } +} diff --git a/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java b/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java new file mode 100644 index 00000000..a58adfc5 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java @@ -0,0 +1,57 @@ +package com.juick.server.xmpp.router; + +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(this); + } +} diff --git a/src/main/java/com/juick/server/xmpp/router/StreamError.java b/src/main/java/com/juick/server/xmpp/router/StreamError.java new file mode 100644 index 00000000..f731f039 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/StreamError.java @@ -0,0 +1,57 @@ +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; + private String text; + + public StreamError() {} + + public StreamError(String condition) { + this.condition = condition; + } + + public static StreamError parse(XmlPullParser parser) throws IOException, XmlPullParserException { + StreamError streamError = new StreamError(); + final int initial = parser.getDepth(); + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initial + 1) { + final String tag = parser.getName(); + final String xmlns = parser.getNamespace(); + if (tag.equals("text") && xmlns.equals(StreamNamespaces.NS_XMPP_STREAMS)) { + streamError.text = XmlUtils.getTagText(parser); + } else if (xmlns.equals(StreamNamespaces.NS_XMPP_STREAMS)) { + streamError.condition = tag; + } else { + XmlUtils.skip(parser); + } + } else if (eventType == XmlPullParser.END_TAG && parser.getDepth() == initial) { + break; + } + } + 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); + } + + public String getText() { + return text; + } +} diff --git a/src/main/java/com/juick/server/xmpp/router/StreamFeatures.java b/src/main/java/com/juick/server/xmpp/router/StreamFeatures.java new file mode 100644 index 00000000..e8fc324f --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/StreamFeatures.java @@ -0,0 +1,95 @@ +/* + * Juick + * Copyright (C) 2008-2013, 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.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * + * @author Ugnich Anton + */ +public class StreamFeatures { + + public static final int NOTAVAILABLE = -1; + public static final int AVAILABLE = 0; + public static final int REQUIRED = 1; + public int STARTTLS = NOTAVAILABLE; + public int ZLIB = NOTAVAILABLE; + public int PLAIN = NOTAVAILABLE; + public int DIGEST_MD5 = NOTAVAILABLE; + public int REGISTER = NOTAVAILABLE; + public int EXTERNAL = NOTAVAILABLE; + + public static StreamFeatures parse(final XmlPullParser parser) throws XmlPullParserException, IOException { + StreamFeatures features = new StreamFeatures(); + final int initial = parser.getDepth(); + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initial + 1) { + final String tag = parser.getName(); + final String xmlns = parser.getNamespace(); + if (tag.equals("starttls") && xmlns != null && xmlns.equals("urn:ietf:params:xml:ns:xmpp-tls")) { + features.STARTTLS = AVAILABLE; + while (parser.next() == XmlPullParser.START_TAG) { + if (parser.getName().equals("required")) { + features.STARTTLS = REQUIRED; + } else { + XmlUtils.skip(parser); + } + } + } else if (tag.equals("compression") && xmlns != null && xmlns.equals("http://jabber.org/features/compress")) { + while (parser.next() == XmlPullParser.START_TAG) { + if (parser.getName().equals("method")) { + final String method = XmlUtils.getTagText(parser).toUpperCase(); + if (method.equals("ZLIB")) { + features.ZLIB = AVAILABLE; + } + } else { + XmlUtils.skip(parser); + } + } + } else if (tag.equals("mechanisms") && xmlns != null && xmlns.equals("urn:ietf:params:xml:ns:xmpp-sasl")) { + while (parser.next() == XmlPullParser.START_TAG) { + if (parser.getName().equals("mechanism")) { + final String mechanism = XmlUtils.getTagText(parser).toUpperCase(); + if (mechanism.equals("PLAIN")) { + features.PLAIN = AVAILABLE; + } else if (mechanism.equals("DIGEST-MD5")) { + features.DIGEST_MD5 = AVAILABLE; + } else if (mechanism.equals("EXTERNAL")) { + features.EXTERNAL = AVAILABLE; + } + } else { + XmlUtils.skip(parser); + } + } + } else if (tag.equals("register") && xmlns != null && xmlns.equals("http://jabber.org/features/iq-register")) { + features.REGISTER = AVAILABLE; + XmlUtils.skip(parser); + } else { + XmlUtils.skip(parser); + } + } else if (eventType == XmlPullParser.END_TAG && parser.getDepth() == initial) { + break; + } + } + return features; + } +} diff --git a/src/main/java/com/juick/server/xmpp/router/StreamHandler.java b/src/main/java/com/juick/server/xmpp/router/StreamHandler.java new file mode 100644 index 00000000..048c61ec --- /dev/null +++ b/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(StreamComponentServer componentServer); + void fail(final Exception ex); + boolean filter(Jid from, Jid to); + void stanzaReceived(String stanza); +} diff --git a/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java b/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java new file mode 100644 index 00000000..1b9b1965 --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/router/XMPPError.java b/src/main/java/com/juick/server/xmpp/router/XMPPError.java new file mode 100644 index 00000000..0cf9a3bc --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java b/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java new file mode 100644 index 00000000..6d67fa9c --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java @@ -0,0 +1,220 @@ +package com.juick.server.xmpp.router; + +import com.juick.server.XMPPServer; +import com.juick.server.xmpp.s2s.BasicXmppSession; +import com.juick.server.xmpp.s2s.CacheEntry; +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.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + +public class XMPPRouter implements StreamHandler { + private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); + + @Inject + private ExecutorService service; + + private final List<StreamComponentServer> connections = Collections.synchronizedList(new ArrayList<>()); + private final List<CacheEntry> outCache = new CopyOnWriteArrayList<>(); + + 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; + } else { + logger.info("bouncing stanza to {} component until it will be ready", hostname); + boolean haveCache = false; + for (CacheEntry entry : outCache) { + if (entry.hostname != null && entry.hostname.equals(hostname)) { + entry.xml += xml; + entry.updated = Instant.now(); + haveCache = true; + break; + } + } + if (!haveCache) { + outCache.add(new CacheEntry(Jid.of(hostname), xml)); + } + } + } + } + } + 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)); + } + } + + 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]; + } + + @Override + public void ready(StreamComponentServer componentServer) { + logger.info("component {} ready", componentServer.to); + String cache = getFromCache(componentServer.to); + if (cache != null) { + logger.debug("sending cache to {}", componentServer.to); + componentServer.send(cache); + } + } + + @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/src/main/java/com/juick/server/xmpp/router/XmlUtils.java b/src/main/java/com/juick/server/xmpp/router/XmlUtils.java new file mode 100644 index 00000000..7579489f --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java b/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java new file mode 100644 index 00000000..ae28f827 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java @@ -0,0 +1,68 @@ +/* + * 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.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) { + super(xmppServiceDomain, configuration); + } + + 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/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java b/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java new file mode 100644 index 00000000..33e875bd --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/s2s/Connection.java b/src/main/java/com/juick/server/xmpp/s2s/Connection.java new file mode 100644 index 00000000..4fa8e741 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/s2s/Connection.java @@ -0,0 +1,158 @@ +/* + * 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_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; + 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; + private boolean authenticated = false; + private boolean trusted = 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; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + public boolean isTrusted() { + return trusted; + } + + public void setTrusted(boolean trusted) { + this.trusted = trusted; + } +} diff --git a/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java b/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java new file mode 100644 index 00000000..72c3ba8d --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java @@ -0,0 +1,231 @@ +/* + * 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.server.xmpp.router.StreamError; +import com.juick.server.xmpp.router.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; +import java.util.stream.Collectors; + +/** + * @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); + setAuthenticated(true); + } 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) && isAuthenticated()) { + 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) && isAuthenticated()) { + 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") && !isAuthenticated()) { + listener.starttls(this); + } else if (isSecured() && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) { + sendOpenStream(null, true); + } else if (isSecured() && tag.equals("auth") && parser.getNamespace().equals(NS_SASL) + && parser.getAttributeValue(null, "mechanism").equals("EXTERNAL") + && !isAuthenticated() && isTrusted()) { + sendStanza("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"); + logger.info("stream {} authenticated externally", streamID); + this.from.add(Jid.of(from)); + setAuthenticated(true); + restartParser(); + } else if (tag.equals("error")) { + StreamError streamError = StreamError.parse(parser); + logger.debug("Stream error {} from {}: {}", streamError.getCondition(), streamID, streamError.getText()); + 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() && !Arrays.asList(xmpp.brokenSSLhosts).contains(from)) { + if (!isSecured()) { + openStream += "<starttls xmlns='" + NS_TLS + "'><optional/></starttls>"; + } else if (!isAuthenticated() && isTrusted()) { + openStream += "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" + + "<mechanism>EXTERNAL</mechanism>" + + "</mechanisms>"; + } + } + 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); + setAuthenticated(true); + } + } + + 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; + } + } + } + logger.warn("rejected from {}, to {}, stream {}", cfrom, cto, from.stream().collect(Collectors.joining(","))); + return false; + } + public void setListener(ConnectionListener listener) { + this.listener = listener; + } +} diff --git a/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java b/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java new file mode 100644 index 00000000..4c32b9ae --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java @@ -0,0 +1,16 @@ +package com.juick.server.xmpp.s2s; + + +import com.juick.server.xmpp.router.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/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java b/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java new file mode 100644 index 00000000..be485ab1 --- /dev/null +++ b/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java @@ -0,0 +1,189 @@ +/* + * 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.router.Stream; +import com.juick.server.xmpp.router.StreamError; +import com.juick.server.xmpp.router.StreamFeatures; +import com.juick.server.xmpp.router.XmlUtils; +import com.juick.server.xmpp.s2s.util.DialbackUtils; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +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; + +import static com.juick.server.xmpp.router.StreamNamespaces.NS_STREAM; +import static com.juick.server.xmpp.s2s.Connection.NS_SASL; + +/** + * @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; + private boolean trusted = 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 if (secured && features.EXTERNAL >=0) { + String authid = Base64.encodeBase64String(from.toEscapedString().getBytes(Charsets.UTF_8)); + send(String.format("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='EXTERNAL'>%s</auth>", authid)); + } else if (secured && streamReady) { + listener.ready(this); + } else { + processDialback(); + } + } else if (tag.equals("proceed") && parser.getNamespace().equals(NS_TLS)) { + listener.proceed(this); + } else if (tag.equals("success") && parser.getNamespace().equals(NS_SASL)) { + streamReady = true; + restartStream(); + sendOpenStream(); + } 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, false); + 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; + } + + public boolean isTrusted() { + return trusted; + } + + public void setTrusted(boolean trusted) { + this.trusted = trusted; + } +} diff --git a/src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java b/src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java new file mode 100644 index 00000000..1367d333 --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java b/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java new file mode 100644 index 00000000..6932298f --- /dev/null +++ b/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/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java b/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java new file mode 100644 index 00000000..d25dbad8 --- /dev/null +++ b/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); + } +} |