diff options
22 files changed, 721 insertions, 39 deletions
diff --git a/juick-common/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/juick-common/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java index 1c520303..6bdb819f 100644 --- a/juick-common/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java +++ b/juick-common/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java @@ -28,21 +28,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.web.multipart.MultipartResolver; -import org.springframework.web.multipart.commons.CommonsMultipartResolver; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; -import org.springframework.web.servlet.resource.ResourceUrlProvider; import rocks.xmpp.core.session.Extension; import rocks.xmpp.core.session.XmppSessionConfiguration; import rocks.xmpp.core.session.debug.LogbackDebugger; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; diff --git a/juick-server-jdbc/src/main/java/com/juick/service/MessagesServiceImpl.java b/juick-server-jdbc/src/main/java/com/juick/service/MessagesServiceImpl.java index 31731466..97db10ec 100644 --- a/juick-server-jdbc/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/juick-server-jdbc/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -869,7 +869,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ , sqlParameterSource) > 0; } if (result) { - getNamedParameterJdbcTemplate().update("UPDATE replies SET replies=replies-1 WHERE message_id=:mid", sqlParameterSource); + getNamedParameterJdbcTemplate().update("UPDATE messages SET replies=replies-1 WHERE message_id=:mid", sqlParameterSource); updateRepliesBy(mid); return true; } diff --git a/juick-server/src/main/java/com/juick/ApiServer.java b/juick-server/src/main/java/com/juick/ApiServer.java index 8b5af8ba..cdd8bb20 100644 --- a/juick-server/src/main/java/com/juick/ApiServer.java +++ b/juick-server/src/main/java/com/juick/ApiServer.java @@ -6,9 +6,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @EnableAutoConfiguration(exclude = { MailSenderAutoConfiguration.class }) +@ComponentScan(basePackages = {"com.juick.server", "com.juick.service"}) public class ApiServer extends SpringBootServletInitializer { @Override diff --git a/juick-server/src/main/java/com/juick/server/CommandsManager.java b/juick-server/src/main/java/com/juick/server/CommandsManager.java index 9178dbb9..6458382f 100644 --- a/juick-server/src/main/java/com/juick/server/CommandsManager.java +++ b/juick-server/src/main/java/com/juick/server/CommandsManager.java @@ -466,7 +466,7 @@ public class CommandsManager { tagService.updateTags(mid, messageTags); return CommandResult.fromString("Tags are updated"); } else { - String attachmentType = StringUtils.isNotEmpty(attachment.toString()) ? attachment.toString().substring(attachment.toString().length() - 3) : null; + String attachmentType = attachment != null && StringUtils.isNotEmpty(attachment.toString()) ? attachment.toString().substring(attachment.toString().length() - 3) : null; int newrid = messagesService.createReply(mid, rid, user.getUid(), txt, attachmentType); if (StringUtils.isNotEmpty(attachmentType)) { String attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost() diff --git a/juick-server/src/main/java/com/juick/server/XMPPConnection.java b/juick-server/src/main/java/com/juick/server/XMPPConnection.java index 406294a1..40cf347a 100644 --- a/juick-server/src/main/java/com/juick/server/XMPPConnection.java +++ b/juick-server/src/main/java/com/juick/server/XMPPConnection.java @@ -36,6 +36,7 @@ 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; @@ -95,7 +96,6 @@ public class XMPPConnection implements StanzaListener, NotificationListener { private CommandsManager commandsManager; @Value("${xmppbot_jid:juick@localhost}") private Jid jid; - @Value("${componentname:localhost}") private String componentName; @Value("${component_port:5347}") @@ -201,6 +201,17 @@ public class XMPPConnection implements StanzaListener, NotificationListener { messagesService.getMessage(jmsg.getMid()))); } } + } else { + String attachment = StringUtils.EMPTY; + OobX oobX = message.getExtension(OobX.class); + if (oobX != null) { + attachment = oobX.getUri().toString(); + } + try { + serverManager.processMessage(userService.getUserByUID(NumberUtils.toInt(message.getFrom().getLocal(), 0)).orElse(new User()), message.getBody(), attachment); + } catch (Exception e1) { + logger.warn("message exception", e1); + } } } else if (jid.getDomain().endsWith(jid.getDomain()) && (jid.getDomain().equals(this.jid.getDomain()) || jid.getDomain().endsWith("." + jid.getDomain()))) { @@ -607,7 +618,11 @@ public class XMPPConnection implements StanzaListener, NotificationListener { if (username.equals(jid.getLocal())) { try { OobX oobX = msg.getExtension(OobX.class); - incomingMessageJuick(user_from, msg.getFrom(), msg.getBody().trim(), oobX.getUri()); + if (oobX != null) { + incomingMessageJuick(user_from, msg.getFrom(), msg.getBody().trim(), oobX.getUri()); + } else { + incomingMessageJuick(user_from, msg.getFrom(), msg.getBody().trim(), null); + } } catch (Exception e) { return; } @@ -669,7 +684,7 @@ public class XMPPConnection implements StanzaListener, NotificationListener { int mid = messagesService.createMessage(user_from.getUid(), body, attachmentType, tags); subscriptionService.subscribeMessage(mid, user_from.getUid()); if (StringUtils.isNotEmpty(attachmentType)) { - String attachmentFName = attachment.getScheme().equals("juick") ? attachment.getPath() + String attachmentFName = attachment.getScheme().equals("juick") ? attachment.getHost() : HttpUtils.downloadImage(attachment.toURL(), tmpDir); String fname = String.format("%d.%s", mid, attachmentType); ImageUtils.saveImageWithPreviews(attachmentFName, fname, tmpDir, imgDir); diff --git a/juick-www/build.gradle b/juick-www/build.gradle index 996b4289..5cebe3e8 100644 --- a/juick-www/build.gradle +++ b/juick-www/build.gradle @@ -38,6 +38,7 @@ dependencies { testCompile("org.springframework.boot:spring-boot-starter-test") testCompile ('net.sourceforge.htmlunit:htmlunit:2.29') + testCompile project(':juick-server') } compileFrontend.dependsOn 'yarn' diff --git a/juick-www/src/main/java/com/juick/Application.java b/juick-www/src/main/java/com/juick/Application.java index f8e5d333..a7a7a654 100644 --- a/juick-www/src/main/java/com/juick/Application.java +++ b/juick-www/src/main/java/com/juick/Application.java @@ -4,10 +4,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Primary; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement +@ComponentScan(basePackages = {"com.juick.www", "com.juick.service"}) public class Application extends SpringBootServletInitializer { @Override diff --git a/juick-www/src/main/java/com/juick/configuration/SapeConfiguration.java b/juick-www/src/main/java/com/juick/www/configuration/SapeConfiguration.java index 2630c05b..68ff28d2 100644 --- a/juick-www/src/main/java/com/juick/configuration/SapeConfiguration.java +++ b/juick-www/src/main/java/com/juick/www/configuration/SapeConfiguration.java @@ -15,7 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.juick.configuration; +package com.juick.www.configuration; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/juick-www/src/main/java/com/juick/configuration/WebSecurityConfig.java b/juick-www/src/main/java/com/juick/www/configuration/WebSecurityConfig.java index 92f43e64..909b6cd3 100644 --- a/juick-www/src/main/java/com/juick/configuration/WebSecurityConfig.java +++ b/juick-www/src/main/java/com/juick/www/configuration/WebSecurityConfig.java @@ -15,7 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.juick.configuration; +package com.juick.www.configuration; import com.juick.service.UserService; import com.juick.service.security.HashParamAuthenticationFilter; @@ -72,7 +72,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { http.addFilterAfter(hashParamAuthenticationFilter(), BasicAuthenticationFilter.class); http .authorizeRequests() - .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/comment") + .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/post2", "/comment") .authenticated() .anyRequest().permitAll() .and() diff --git a/juick-www/src/main/java/com/juick/configuration/WwwAppConfiguration.java b/juick-www/src/main/java/com/juick/www/configuration/WwwAppConfiguration.java index ea585a6f..13a394cb 100644 --- a/juick-www/src/main/java/com/juick/configuration/WwwAppConfiguration.java +++ b/juick-www/src/main/java/com/juick/www/configuration/WwwAppConfiguration.java @@ -15,7 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.juick.configuration; +package com.juick.www.configuration; import com.juick.server.configuration.BaseWebConfiguration; import com.juick.server.configuration.StorageConfiguration; diff --git a/juick-www/src/main/java/com/juick/configuration/XMPPConfiguration.java b/juick-www/src/main/java/com/juick/www/configuration/XMPPConfiguration.java index 91f55759..1396f9f9 100644 --- a/juick-www/src/main/java/com/juick/configuration/XMPPConfiguration.java +++ b/juick-www/src/main/java/com/juick/www/configuration/XMPPConfiguration.java @@ -1,4 +1,4 @@ -package com.juick.configuration; +package com.juick.www.configuration; import com.juick.Message; import org.slf4j.Logger; diff --git a/juick-www/src/main/java/com/juick/www/controllers/Messages.java b/juick-www/src/main/java/com/juick/www/controllers/MessagesWWW.java index 65e122a6..e6662c4e 100644 --- a/juick-www/src/main/java/com/juick/www/controllers/Messages.java +++ b/juick-www/src/main/java/com/juick/www/controllers/MessagesWWW.java @@ -52,7 +52,7 @@ import java.util.stream.Collectors; * @author Ugnich Anton */ @Controller -public class Messages { +public class MessagesWWW { @Inject private UserService userService; @Inject diff --git a/juick-www/src/main/java/com/juick/www/controllers/NewMessage.java b/juick-www/src/main/java/com/juick/www/controllers/NewMessage.java index f8a11d82..6e16ae50 100644 --- a/juick-www/src/main/java/com/juick/www/controllers/NewMessage.java +++ b/juick-www/src/main/java/com/juick/www/controllers/NewMessage.java @@ -16,13 +16,10 @@ */ package com.juick.www.controllers; -import com.juick.Status; -import com.juick.Tag; import com.juick.User; import com.juick.server.helpers.AnonymousUser; import com.juick.server.util.*; import com.juick.service.*; -import com.juick.util.MessageUtils; import com.juick.www.WebApp; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; @@ -34,7 +31,6 @@ import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import rocks.xmpp.addr.Jid; import rocks.xmpp.core.stanza.model.Message; @@ -47,7 +43,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.List; import java.util.stream.Collectors; /** @@ -76,6 +71,8 @@ public class NewMessage { private String imgDir; @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String tmpDir; + @Value("${xmppbot_jid:juick@localhost}") + private Jid botJid; private static final Logger logger = LoggerFactory.getLogger(NewMessage.class); @@ -247,11 +244,11 @@ public class NewMessage { if (visitor.getUid() == 0 || visitor.isBanned()) { throw new HttpForbiddenException(); } - String body = bodyParam.replace("\r", StringUtils.EMPTY); + String body = StringUtils.isNotEmpty(bodyParam) ? bodyParam.replace("\r", StringUtils.EMPTY) : StringUtils.EMPTY; String attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); - if (StringUtils.isBlank(attachmentFName) && img != null && img.length() > 10) { + if (StringUtils.isBlank(attachmentFName) && StringUtils.isNotBlank(img)) { try { URL imgUrl = new URL(img); attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir); @@ -263,7 +260,7 @@ public class NewMessage { Message msg = new Message(); msg.setType(Message.Type.CHAT); msg.setFrom(Jid.of(String.valueOf(visitor.getUid()), "uid.juick.com", "perl")); - msg.setTo(Jid.of("juick@juick.com/Juick")); + msg.setTo(botJid); msg.setBody(body); try { if (StringUtils.isNotEmpty(attachmentFName)) { diff --git a/juick-www/src/test/java/com/juick/WebAppTests.java b/juick-www/src/test/java/com/juick/WebAppTests.java index e9908acd..43198859 100644 --- a/juick-www/src/test/java/com/juick/WebAppTests.java +++ b/juick-www/src/test/java/com/juick/WebAppTests.java @@ -26,6 +26,11 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.juick.Message; import com.juick.Tag; import com.juick.User; +import com.juick.server.XMPPConnection; +import com.juick.server.XMPPRouter; +import com.juick.server.XMPPServer; +import com.juick.server.configuration.ApiAppConfiguration; +import com.juick.server.configuration.BaseWebConfiguration; import com.juick.service.*; import com.juick.util.MessageUtils; import com.juick.www.WebApp; @@ -38,9 +43,14 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; @@ -49,6 +59,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.FileSystemUtils; +import org.springframework.web.servlet.resource.ResourceUrlProvider; import javax.inject.Inject; import javax.servlet.http.Cookie; @@ -74,9 +85,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @AutoConfigureMockMvc -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@TestPropertySource(properties = {"xmpp_disabled=true"}) - +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = {Application.class, XMPPRouter.class}) public class WebAppTests { @MockBean private ImagesService imagesService; @@ -239,30 +248,37 @@ public class WebAppTests { } @Test public void postMessageTests() throws Exception { - mockMvc.perform(post("/post").param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ApiServer.class) + .properties("server.port=8081") + .run(); + XMPPServer xmpp = context.getBean(XMPPServer.class); + assertThat(xmpp.getInConnections().size(), is(0)); + mockMvc.perform(post("/post2").param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); MvcResult loginResult = mockMvc.perform(post("/login") .param("username", ugnichName) .param("password", ugnichPassword)).andReturn(); - mockMvc.perform(post("/post") - .cookie(loginResult.getResponse().getCookies()) - .param("wrong_param", "yo")).andExpect(status().isBadRequest()); String msgText = "yoppppl"; - mockMvc.perform(post("/post") + mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) - .param("body", msgText)).andExpect(status().isOk()); + .param("body", msgText)).andExpect(status().isFound()); + Thread.sleep(5000); Message lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); assertThat(lastMessage.getText(), equalTo(msgText)); - mockMvc.perform(post("/post") + mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) - .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isOk()); - mockMvc.perform(post("/post") + .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isFound()); + Thread.sleep(5000); + lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); + assertThat(lastMessage.getAttachmentType(), equalTo("png")); + mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) .param("img", "bad_url")).andExpect(status().isBadRequest()); FileInputStream fi = new FileInputStream(new ClassPathResource("static/tagscloud.png").getFile()); MockMultipartFile file = new MockMultipartFile("attach", fi); - mockMvc.perform(multipart("/post") + mockMvc.perform(multipart("/post2") .file(file) - .cookie(loginResult.getResponse().getCookies())).andExpect(status().isOk()); + .cookie(loginResult.getResponse().getCookies())).andExpect(status().isFound()); int mid = messagesService.createMessage(ugnich.getUid(), "dummy message", null, null); mockMvc.perform(post("/comment") .param("mid", String.valueOf(mid)) @@ -286,6 +302,12 @@ public class WebAppTests { .cookie(loginResult.getResponse().getCookies()) .param("mid", String.valueOf(mid)) .param("body", "yo")).andExpect(redirectedUrl(String.format("/%s/%d#%d", ugnichName, mid, 3))); + mockMvc.perform(post("/post2") + .cookie(loginResult.getResponse().getCookies()) + .param("body", String.format("D #%d/%d", mid, 3))) + .andExpect(status().isFound()); + Thread.sleep(5000); + assertThat(messagesService.getReplies(mid).size(), equalTo(2)); } @Test public void hashLoginShouldNotUseSession() throws Exception { diff --git a/juick-www/src/test/java/com/juick/server/Stream.java b/juick-www/src/test/java/com/juick/server/Stream.java new file mode 100644 index 00000000..9dbea3b2 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/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; + +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-www/src/test/java/com/juick/server/StreamComponentServer.java b/juick-www/src/test/java/com/juick/server/StreamComponentServer.java new file mode 100644 index 00000000..8c66c2e8 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/StreamComponentServer.java @@ -0,0 +1,61 @@ +package com.juick.server; + +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; + +import static com.juick.server.StreamNamespaces.NS_COMPONENT_ACCEPT; +import static com.juick.server.StreamNamespaces.NS_STREAM; + +/** + * 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(NS_COMPONENT_ACCEPT) + || !parser.getNamespace("stream").equals(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'>", NS_STREAM, 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-www/src/test/java/com/juick/server/StreamError.java b/juick-www/src/test/java/com/juick/server/StreamError.java new file mode 100644 index 00000000..d552b590 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/StreamError.java @@ -0,0 +1,46 @@ +package com.juick.server; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +import static com.juick.server.StreamNamespaces.NS_XMPP_STREAMS; + + +/** + * 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(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, NS_XMPP_STREAMS); + } +} diff --git a/juick-www/src/test/java/com/juick/server/StreamHandler.java b/juick-www/src/test/java/com/juick/server/StreamHandler.java new file mode 100644 index 00000000..d11fba1f --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/StreamHandler.java @@ -0,0 +1,13 @@ +package com.juick.server; + +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-www/src/test/java/com/juick/server/StreamNamespaces.java b/juick-www/src/test/java/com/juick/server/StreamNamespaces.java new file mode 100644 index 00000000..fbedcae6 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/StreamNamespaces.java @@ -0,0 +1,10 @@ +package com.juick.server; + +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-www/src/test/java/com/juick/server/XMPPError.java b/juick-www/src/test/java/com/juick/server/XMPPError.java new file mode 100644 index 00000000..66e4ec44 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/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; + +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-www/src/test/java/com/juick/server/XMPPRouter.java b/juick-www/src/test/java/com/juick/server/XMPPRouter.java new file mode 100644 index 00000000..d03a0880 --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/XMPPRouter.java @@ -0,0 +1,173 @@ +package com.juick.server; + +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.Stanza; +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; + + @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; + } + logger.error("component unavailable: {}", hostname); + + } + + 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) { + sendOut(parse(stanza)); + } + + @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-www/src/test/java/com/juick/server/XmlUtils.java b/juick-www/src/test/java/com/juick/server/XmlUtils.java new file mode 100644 index 00000000..85fd352c --- /dev/null +++ b/juick-www/src/test/java/com/juick/server/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; + +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(); + } +} |