From ae5870f1fa9bbf045f1881664bb8a3a098fd2610 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 11 Dec 2018 16:14:14 +0300 Subject: avatar upload api --- src/test/java/com/juick/server/tests/ServerTests.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'src/test/java') diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index deef6f30..b732cb98 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -44,6 +44,7 @@ import com.juick.server.api.webfinger.model.Account; import com.juick.server.api.xnodeinfo2.model.NodeInfo; import com.juick.server.util.HttpUtils; import com.juick.server.util.ImageUtils; +import com.juick.server.www.WebApp; import com.juick.server.xmpp.helpers.XMPPStatus; import com.juick.server.xmpp.s2s.ConnectionIn; import com.juick.service.*; @@ -76,6 +77,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.DigestUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.LinkedMultiValueMap; @@ -204,6 +206,8 @@ public class ServerTests { private SignatureManager signatureManager; @Inject private ActivityPubManager activityPubManager; + @Inject + private WebApp webApp; private static User ugnich, freefd, juick; static String ugnichName, ugnichPassword, freefdName, freefdPassword, juickName, juickPassword; @@ -1858,4 +1862,19 @@ public class ServerTests { jdbcTemplate.update("INSERT INTO facebook(user_id, fb_id) VALUES(?, ?)", ugnich.getUid(), "100001866137681"); assertThat(userService.getUserByName("ugnich").isVerified(), is(true)); } + @Test + public void avatarUploadOverApi() throws Exception { + ClassPathResource defaultAvatar = new ClassPathResource("static/av-96.png"); + String hash = DigestUtils.md5DigestAsHex(IOUtils.toByteArray(defaultAvatar.getInputStream())); + assertThat(webApp.getAvatarUrl(userService.getUserByName(freefdName)), is(String.format("http://localhost:8080/av-96-%s.png", hash))); + + ClassPathResource newAvatar = new ClassPathResource("cmyk.jpg"); + byte[] newAvatarData = IOUtils.toByteArray(newAvatar.getInputStream()); + mockMvc.perform(MockMvcRequestBuilders.multipart("/api/me/upload") + .file("avatar", newAvatarData) + .with(httpBasic(freefdName, freefdPassword)) + ).andExpect(status().isOk()); + String newHash = DigestUtils.md5DigestAsHex(newAvatarData); + assertThat(webApp.getAvatarUrl(userService.getUserByName(freefdName)), is(String.format("http://localhost:8080/i/a/%d-%s.png", freefd.getUid(), newHash))); + } } -- cgit v1.2.3 From 1dc570a8aaaa8657eb4faa7283e30a5fde93d321 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 12 Dec 2018 09:19:53 +0300 Subject: Vary: Accept-Language --- src/main/java/com/juick/server/www/VaryHandler.java | 14 ++++++++++++++ src/test/java/com/juick/server/tests/ServerTests.java | 12 ++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/java/com/juick/server/www/VaryHandler.java (limited to 'src/test/java') diff --git a/src/main/java/com/juick/server/www/VaryHandler.java b/src/main/java/com/juick/server/www/VaryHandler.java new file mode 100644 index 00000000..5a1b86a6 --- /dev/null +++ b/src/main/java/com/juick/server/www/VaryHandler.java @@ -0,0 +1,14 @@ +package com.juick.server.www; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +import javax.servlet.http.HttpServletResponse; + +@ControllerAdvice +public class VaryHandler { + @ModelAttribute + public void setVaryResponseHeader(HttpServletResponse response) { + response.setHeader("Vary", "Accept-Language"); + } +} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index b732cb98..b914aed0 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -1877,4 +1877,16 @@ public class ServerTests { String newHash = DigestUtils.md5DigestAsHex(newAvatarData); assertThat(webApp.getAvatarUrl(userService.getUserByName(freefdName)), is(String.format("http://localhost:8080/i/a/%d-%s.png", freefd.getUid(), newHash))); } + @Test + public void varyMvcResponse() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("Vary", "Accept-Language")); + mockMvc.perform(get("/rss/ugnich/blog")) + .andExpect(status().isOk()) + .andExpect(header().string("Vary", "Accept-Language")); + mockMvc.perform(get("/api/messages")) + .andExpect(status().isOk()) + .andExpect(header().string("Vary", "Accept-Language")); + } } -- cgit v1.2.3 From f8603ba2dcfc311e3ed9b6f7ade23db204f2d99a Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 12 Dec 2018 10:01:30 +0300 Subject: Fix avatar tests --- src/test/java/com/juick/server/tests/ServerTests.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/test/java') diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index b914aed0..01aa298b 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -221,10 +221,16 @@ public class ServerTests { FileSystemUtils.deleteRecursively(Paths.get(imgDir, "photos-1024")); FileSystemUtils.deleteRecursively(Paths.get(imgDir, "photos-512")); FileSystemUtils.deleteRecursively(Paths.get(imgDir, "ps")); + FileSystemUtils.deleteRecursively(Paths.get(imgDir, "a")); + FileSystemUtils.deleteRecursively(Paths.get(imgDir, "ao")); + FileSystemUtils.deleteRecursively(Paths.get(imgDir, "as")); Files.createDirectory(Paths.get(imgDir, "p")); Files.createDirectory(Paths.get(imgDir, "photos-1024")); Files.createDirectory(Paths.get(imgDir, "photos-512")); Files.createDirectory(Paths.get(imgDir, "ps")); + Files.createDirectory(Paths.get(imgDir, "a")); + Files.createDirectory(Paths.get(imgDir, "ao")); + Files.createDirectory(Paths.get(imgDir, "as")); if (!isSetUp) { ugnichName = "ugnich"; ugnichPassword = "secret"; @@ -1868,14 +1874,15 @@ public class ServerTests { String hash = DigestUtils.md5DigestAsHex(IOUtils.toByteArray(defaultAvatar.getInputStream())); assertThat(webApp.getAvatarUrl(userService.getUserByName(freefdName)), is(String.format("http://localhost:8080/av-96-%s.png", hash))); - ClassPathResource newAvatar = new ClassPathResource("cmyk.jpg"); + ClassPathResource newAvatar = new ClassPathResource("static/durov.png"); byte[] newAvatarData = IOUtils.toByteArray(newAvatar.getInputStream()); mockMvc.perform(MockMvcRequestBuilders.multipart("/api/me/upload") .file("avatar", newAvatarData) .with(httpBasic(freefdName, freefdPassword)) ).andExpect(status().isOk()); String newHash = DigestUtils.md5DigestAsHex(newAvatarData); - assertThat(webApp.getAvatarUrl(userService.getUserByName(freefdName)), is(String.format("http://localhost:8080/i/a/%d-%s.png", freefd.getUid(), newHash))); + URI newUri = Paths.get(imgDir, "ao", String.format("%d.png", freefd.getUid())).toUri(); + assertThat(DigestUtils.md5DigestAsHex(IOUtils.toByteArray(newUri)), is(newHash)); } @Test public void varyMvcResponse() throws Exception { -- cgit v1.2.3 From 9d493ac966db2e016f73efcdcfb65dfb10bf8114 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 12 Dec 2018 15:12:27 +0300 Subject: UserInfo -> User --- src/main/java/com/juick/User.java | 27 ++++++++++ src/main/java/com/juick/model/UserInfo.java | 60 ---------------------- src/main/java/com/juick/server/XMPPConnection.java | 4 +- src/main/java/com/juick/server/api/Users.java | 14 +---- .../juick/server/configuration/SecurityConfig.java | 2 +- .../server/www/controllers/AnythingFilter.java | 2 +- .../com/juick/server/www/controllers/Settings.java | 12 ++--- src/main/java/com/juick/service/UserService.java | 5 +- .../java/com/juick/service/UserServiceImpl.java | 37 ++++++------- .../java/com/juick/server/tests/ServerTests.java | 20 ++++++-- 10 files changed, 74 insertions(+), 109 deletions(-) delete mode 100644 src/main/java/com/juick/model/UserInfo.java (limited to 'src/test/java') diff --git a/src/main/java/com/juick/User.java b/src/main/java/com/juick/User.java index 7221e416..78d47139 100644 --- a/src/main/java/com/juick/User.java +++ b/src/main/java/com/juick/User.java @@ -53,6 +53,9 @@ public class User { private URI uri; private Instant seen; private boolean verified; + private String country; + private String url; + private String description; public User() { tokens = new ArrayList<>(); @@ -232,4 +235,28 @@ public class User { public void setVerified(boolean verified) { this.verified = verified; } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } } diff --git a/src/main/java/com/juick/model/UserInfo.java b/src/main/java/com/juick/model/UserInfo.java deleted file mode 100644 index ca5d75e0..00000000 --- a/src/main/java/com/juick/model/UserInfo.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.model; - -/** - * Created by vt on 03/09/16. - */ -public class UserInfo { - private String fullName; - private String country; - private String url; - private String description; - - public String getFullName() { - return fullName; - } - - public void setFullName(String fullName) { - this.fullName = fullName; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} diff --git a/src/main/java/com/juick/server/XMPPConnection.java b/src/main/java/com/juick/server/XMPPConnection.java index f77b2354..dfbe92d5 100644 --- a/src/main/java/com/juick/server/XMPPConnection.java +++ b/src/main/java/com/juick/server/XMPPConnection.java @@ -22,7 +22,6 @@ import com.juick.formatters.PlainTextFormatter; import com.juick.server.www.WebApp; import com.juick.service.component.*; import com.juick.model.CommandResult; -import com.juick.model.UserInfo; import com.juick.server.xmpp.iq.MessageQuery; import com.juick.server.xmpp.s2s.BasicXmppSession; import com.juick.server.xmpp.s2s.StanzaListener; @@ -35,7 +34,6 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; import rocks.xmpp.addr.Jid; import rocks.xmpp.core.XmppException; import rocks.xmpp.core.session.XmppSession; @@ -164,7 +162,7 @@ public class XMPPConnection implements StanzaListener, NotificationListener { } User user = userService.getUserByName(iq.getTo().getLocal()); if (!user.isAnonymous()) { - UserInfo info = userService.getUserInfo(user); + User info = userService.getUserInfo(user); VCard userVCard = new VCard(); userVCard.setFormattedName(info.getFullName()); userVCard.setNickname(user.getName()); diff --git a/src/main/java/com/juick/server/api/Users.java b/src/main/java/com/juick/server/api/Users.java index 0e0fee85..5c6efe0c 100644 --- a/src/main/java/com/juick/server/api/Users.java +++ b/src/main/java/com/juick/server/api/Users.java @@ -19,7 +19,6 @@ package com.juick.server.api; import com.juick.User; import com.juick.model.ApplicationStatus; -import com.juick.model.UserInfo; import com.juick.server.util.*; import com.juick.server.www.WebApp; import com.juick.service.*; @@ -96,7 +95,7 @@ public class Users { me.setRead(userService.getUserFriends(visitor.getUid())); me.setReaders(userService.getUserReaders(visitor.getUid())); me.setAvatar(webApp.getAvatarUrl(visitor)); - return me; + return (SecureUser)userService.getUserInfo(me); } @PostMapping("/api/me/upload") public void updateInfo(@RequestParam MultipartFile avatar) throws IOException { @@ -111,9 +110,6 @@ public class Users { public List doGetUserRead( @RequestParam String uname) { User visitor = UserUtils.getCurrentUser(); - if (visitor.isAnonymous()) { - throw new HttpForbiddenException(); - } int uid = 0; if (uname == null) { uid = visitor.getUid(); @@ -138,9 +134,6 @@ public class Users { public List doGetUserReaders( @RequestParam String uname) { User visitor = UserUtils.getCurrentUser(); - if (visitor.isAnonymous()) { - throw new HttpForbiddenException(); - } int uid = 0; if (uname == null) { uid = visitor.getUid(); @@ -162,7 +155,7 @@ public class Users { } @GetMapping("/api/info/{uname}") - public UserInfo getUserInfo(@PathVariable String uname) { + public User getUserInfo(@PathVariable String uname) { User user = userService.getUserByName(uname); if (!user.isBanned()) { user.setRead(doGetUserRead(uname)); @@ -177,9 +170,6 @@ public class Users { public String getHash() { return getAuthHash(); } - public UserInfo getUserInfo() { - return userService.getUserInfo(this); - } public List getJIDs() { return userService.getAllJIDs(this); } diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index f53cc531..16b61172 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -98,7 +98,7 @@ public class SecurityConfig { .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events").permitAll() + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events", "/api/info/**").permitAll() .anyRequest().hasRole("USER") .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) diff --git a/src/main/java/com/juick/server/www/controllers/AnythingFilter.java b/src/main/java/com/juick/server/www/controllers/AnythingFilter.java index cdbeafc0..57b298eb 100644 --- a/src/main/java/com/juick/server/www/controllers/AnythingFilter.java +++ b/src/main/java/com/juick/server/www/controllers/AnythingFilter.java @@ -60,7 +60,7 @@ public class AnythingFilter extends OncePerRequestFilter { } else { com.juick.User user = userService.getUserByName(anything); if (!user.isAnonymous()) { - ((HttpServletResponse) servletResponse).sendRedirect("/" + user.getName() + "/?before=" + before); + servletResponse.sendRedirect("/" + user.getName() + "/?before=" + before); } else { filterChain.doFilter(servletRequest, servletResponse); } diff --git a/src/main/java/com/juick/server/www/controllers/Settings.java b/src/main/java/com/juick/server/www/controllers/Settings.java index fc84b410..d5a21d09 100644 --- a/src/main/java/com/juick/server/www/controllers/Settings.java +++ b/src/main/java/com/juick/server/www/controllers/Settings.java @@ -18,7 +18,6 @@ package com.juick.server.www.controllers; import com.juick.User; import com.juick.model.NotifyOpts; -import com.juick.model.UserInfo; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.HttpUtils; import com.juick.server.util.UserUtils; @@ -156,16 +155,15 @@ public class Settings { } break; case "about": - UserInfo info = new UserInfo(); - info.setFullName(request.getParameter("fullname")); - info.setCountry(request.getParameter("country")); - info.setUrl(request.getParameter("url")); - info.setDescription(request.getParameter("descr")); + visitor.setFullName(request.getParameter("fullname")); + visitor.setCountry(request.getParameter("country")); + visitor.setUrl(request.getParameter("url")); + visitor.setDescription(request.getParameter("descr")); String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, tmpDir).getHost(); if (StringUtils.isNotEmpty(avatarTmpPath)) { imagesService.saveAvatar(avatarTmpPath, visitor.getUid()); } - if (userService.updateUserInfo(visitor, info)) { + if (userService.updateUserInfo(visitor)) { result = String.format("

Your info is updated.

Back to blog.

", visitor.getName()); } break; diff --git a/src/main/java/com/juick/service/UserService.java b/src/main/java/com/juick/service/UserService.java index 832f978a..3a51dffb 100644 --- a/src/main/java/com/juick/service/UserService.java +++ b/src/main/java/com/juick/service/UserService.java @@ -20,7 +20,6 @@ package com.juick.service; import com.juick.Message; import com.juick.User; import com.juick.model.Auth; -import com.juick.model.UserInfo; import javax.annotation.Nonnull; import java.time.Instant; @@ -75,9 +74,9 @@ public interface UserService { int setUserOptionInt(int uid, String option, int value); - UserInfo getUserInfo(User user); + User getUserInfo(User user); - boolean updateUserInfo(User user, UserInfo info); + boolean updateUserInfo(User info); boolean getCanMedia(int uid); diff --git a/src/main/java/com/juick/service/UserServiceImpl.java b/src/main/java/com/juick/service/UserServiceImpl.java index 93904139..95a13f65 100644 --- a/src/main/java/com/juick/service/UserServiceImpl.java +++ b/src/main/java/com/juick/service/UserServiceImpl.java @@ -21,7 +21,6 @@ import com.juick.Message; import com.juick.User; import com.juick.model.AnonymousUser; import com.juick.model.Auth; -import com.juick.model.UserInfo; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -375,29 +374,31 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { @Transactional(readOnly = true) @Override - public UserInfo getUserInfo(final User user) { - List list = getJdbcTemplate().query( - "SELECT fullname, country, url, descr FROM usersinfo WHERE user_id = ?", - ((rs, rowNum) -> { - UserInfo info = new UserInfo(); - info.setFullName(rs.getString(1)); - info.setCountry(rs.getString(2)); - info.setUrl(rs.getString(3)); - info.setDescription(rs.getString(4)); - return info; - }), - user.getUid()); - - return list.isEmpty() ? new UserInfo() : list.get(0); + public User getUserInfo(final User user) { + try { + getJdbcTemplate().queryForObject( + "SELECT fullname, country, url, descr FROM usersinfo WHERE user_id = ?", + ((rs, rowNum) -> { + user.setFullName(rs.getString(1)); + user.setCountry(rs.getString(2)); + user.setUrl(rs.getString(3)); + user.setDescription(rs.getString(4)); + return user; + }), + user.getUid()); + } catch (EmptyResultDataAccessException e) { + return user; + } + return user; } @Transactional @Override - public boolean updateUserInfo(final User user, final UserInfo info) { + public boolean updateUserInfo(final User info) { try { return getJdbcTemplate().update( "INSERT INTO usersinfo(user_id, fullname, country, url, descr) VALUES (?, ?, ?, ?, ?)", - user.getUid(), + info.getUid(), info.getFullName(), info.getCountry(), info.getUrl(), @@ -408,7 +409,7 @@ public class UserServiceImpl extends BaseJdbcService implements UserService { info.getCountry(), info.getUrl(), info.getDescription(), - user.getUid()) > 0; + info.getUid()) > 0; } } diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 01aa298b..d1cdac8b 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -27,10 +27,7 @@ import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.jayway.jsonpath.JsonPath; import com.juick.*; -import com.juick.model.AnonymousUser; -import com.juick.model.CommandResult; -import com.juick.model.PrivateChats; -import com.juick.model.TagStats; +import com.juick.model.*; import com.juick.server.*; import com.juick.server.api.activity.model.Context; import com.juick.server.api.activity.model.activities.Create; @@ -1896,4 +1893,19 @@ public class ServerTests { .andExpect(status().isOk()) .andExpect(header().string("Vary", "Accept-Language")); } + @Test + public void apiInfo() throws Exception { + userService.createUser("tst", "tst"); + MvcResult result = mockMvc.perform(get("/api/info/tst")) + .andExpect(status().isOk()) + .andReturn(); + User tst = jsonMapper.readValue(result.getResponse().getContentAsString(), User.class); + assertThat(tst.getReaders(), is(nullValue())); + commandsManager.processCommand(ugnich, "S @tst", emptyUri); + result = mockMvc.perform(get("/api/info/tst")) + .andExpect(status().isOk()) + .andReturn(); + tst = jsonMapper.readValue(result.getResponse().getContentAsString(), User.class); + assertThat(tst.getReaders().size(), is(1)); + } } -- cgit v1.2.3 From 445e6bbc61da68149ccd12e05cf781cffcf05aee Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 13 Dec 2018 10:49:16 +0300 Subject: do not count linebreak as part of entity --- src/main/java/com/juick/util/MessageUtils.java | 41 +++++++++++++++++--------- src/test/java/com/juick/MessageTest.java | 4 +++ 2 files changed, 31 insertions(+), 14 deletions(-) (limited to 'src/test/java') diff --git a/src/main/java/com/juick/util/MessageUtils.java b/src/main/java/com/juick/util/MessageUtils.java index cc0d7b12..5669a454 100644 --- a/src/main/java/com/juick/util/MessageUtils.java +++ b/src/main/java/com/juick/util/MessageUtils.java @@ -94,29 +94,29 @@ public class MessageUtils { String txt = msg.getText(); // http://juick.com/last?page=2 - List result = new ArrayList<>(entitiesForType("a", txt, urlWithWhitespacesRegex, matcher -> matcher.group(3), matcher -> Optional.of(matcher.group(2)))); + List result = new ArrayList<>(entitiesForType("a", txt, urlWithWhitespacesRegex, matcher -> matcher.group(3), matcher -> Optional.of(matcher.group(2)), 0)); // [link text][http://juick.com/last?page=2] - result.addAll(entitiesForType("a", txt, textUrlRegex, matcher -> matcher.group(1), matcher -> Optional.of(matcher.group(2)))); + result.addAll(entitiesForType("a", txt, textUrlRegex, matcher -> matcher.group(1), matcher -> Optional.of(matcher.group(2)), 0)); // [link text](http://juick.com/last?page=2) - result.addAll(entitiesForType("a", txt, textUrlRegex2, matcher -> matcher.group(1), matcher -> Optional.of(matcher.group(2)))); + result.addAll(entitiesForType("a", txt, textUrlRegex2, matcher -> matcher.group(1), matcher -> Optional.of(matcher.group(2)), 0)); // #12345 - result.addAll(entitiesForType("a", txt, midRegex, matcher -> String.format("#%s", matcher.group(2)), matcher -> Optional.of("https://juick.com/m/" + matcher.group(2)))); + result.addAll(entitiesForType("a", txt, midRegex, matcher -> String.format("#%s", matcher.group(2)), matcher -> Optional.of("https://juick.com/m/" + matcher.group(2)), 0)); // #12345/65 - result.addAll(entitiesForType("a", txt, ridRegex, matcher -> String.format("#%s/%s", matcher.group(2), matcher.group(3)), matcher -> Optional.of(String.format("https://juick.com/m/%s#%s", matcher.group(2), matcher.group(3))))); + result.addAll(entitiesForType("a", txt, ridRegex, matcher -> String.format("#%s/%s", matcher.group(2), matcher.group(3)), matcher -> Optional.of(String.format("https://juick.com/m/%s#%s", matcher.group(2), matcher.group(3))), 0)); // /12 - result.addAll(entitiesForType("a", txt, replyNumberRegex, matcher -> "/" + matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/m/%d#%s", msg.getMid(), matcher.group(2))))); + result.addAll(entitiesForType("a", txt, replyNumberRegex, matcher -> "/" + matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/m/%d#%s", msg.getMid(), matcher.group(2))), 0)); // @username@jabber.org - result.addAll(entitiesForType("a", txt, jidRegex, matcher -> matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/%s", matcher.group(2))))); + result.addAll(entitiesForType("a", txt, jidRegex, matcher -> matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/%s", matcher.group(2))), 0)); // @username - result.addAll(entitiesForType("a", txt, usernameRegex, matcher -> matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/%s", matcher.group(2))))); + result.addAll(entitiesForType("a", txt, usernameRegex, matcher -> matcher.group(2), matcher -> Optional.of(String.format("https://juick.com/%s", matcher.group(2))), 0)); // *bold* - result.addAll(entitiesForType("b", txt, boldRegex, matcher -> matcher.group(2), matcher -> Optional.empty())); + result.addAll(entitiesForType("b", txt, boldRegex, matcher -> matcher.group(2), matcher -> Optional.empty(), 0)); // /italic/ - result.addAll(entitiesForType("i", txt, italicRegex, matcher -> matcher.group(2), matcher -> Optional.empty())); + result.addAll(entitiesForType("i", txt, italicRegex, matcher -> matcher.group(2), matcher -> Optional.empty(), 0)); // _underline_ - result.addAll(entitiesForType("u", txt, underlineRegex, matcher -> matcher.group(2), matcher -> Optional.empty())); + result.addAll(entitiesForType("u", txt, underlineRegex, matcher -> matcher.group(2), matcher -> Optional.empty(), 0)); // > citate - result.addAll(entitiesForType("q", txt, citateRegex, matcher -> matcher.group(1), matcher -> Optional.empty())); + result.addAll(entitiesForType("q", txt, citateRegex, matcher -> matcher.group(1), matcher -> Optional.empty(), 1)); return result; } @@ -369,7 +369,20 @@ public class MessageUtils { return collectMatches(jidPattern, msg.getText()); } - private static List entitiesForType(String type, String input, String patternText, Function textGroup, Function> linkGroup) { + /** + * + * @param type Name of the entity + * @param input data to find matches + * @param patternText pattern to match + * @param textGroup function which return text representation + * @param linkGroup function which return link address + * @param endGroupId group id used to set end of entity (e.g. do not count linebreak as part of quote entity) + * @return list of entities + */ + private static List entitiesForType(String type, String input, String patternText, + Function textGroup, + Function> linkGroup, + int endGroupId) { List result = new ArrayList<>(); Pattern pattern = Pattern.compile(patternText); Matcher matcher = pattern.matcher(input); @@ -380,7 +393,7 @@ public class MessageUtils { Optional link = linkGroup.apply(matcher); link.ifPresent(entity::setUrl); entity.setStart(matcher.start()); - entity.setEnd(matcher.end()); + entity.setEnd(matcher.end(endGroupId)); result.add(entity); } return result; diff --git a/src/test/java/com/juick/MessageTest.java b/src/test/java/com/juick/MessageTest.java index 1d59a8db..4c37a678 100644 --- a/src/test/java/com/juick/MessageTest.java +++ b/src/test/java/com/juick/MessageTest.java @@ -191,5 +191,9 @@ public class MessageTest { assertThat(entities.stream().filter(e -> e.getType().equals("q")).count(), is(1L)); assertThat(entities.stream().filter(e -> e.getType().equals("u")).count(), is(1L)); assertThat(entities.stream().filter(e -> e.getType().equals("a")).count(), is(4L)); + Entity yo = entities.stream().filter(e -> e.getType().equals("q")).findFirst().orElseThrow(IllegalStateException::new); + assertThat(yo.getText(), is("how ?")); + assertThat(yo.getStart(), is(0)); + assertThat(yo.getEnd(), is(7)); } } -- cgit v1.2.3 From 8268ccc461608d45bdf60e58ccf49256e8cc993c Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 17 Dec 2018 11:58:50 +0300 Subject: CORS for ActivityPub endpoints --- .../juick/server/configuration/SecurityConfig.java | 30 ++++++++++++---------- .../java/com/juick/server/tests/ServerTests.java | 5 ++++ 2 files changed, 21 insertions(+), 14 deletions(-) (limited to 'src/test/java') diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index 16b61172..7145e9d5 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -69,6 +69,20 @@ public class SecurityConfig { public UserDetailsService userDetailsService() { return new JuickUserDetailsService(userService); } + @Bean + static CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); + configuration.setAllowedHeaders(Collections.singletonList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", configuration); + source.registerCorsConfiguration("/u/**", configuration); + source.registerCorsConfiguration("/n/**", configuration); + return source; + } @Configuration @Order(1) @@ -98,7 +112,7 @@ public class SecurityConfig { .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", - "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/u/**", "/.well-known/webfinger", "/.well-known/x-nodeinfo2", "/rss/**", "/api/events", "/api/info/**").permitAll() + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/signup", "/api/inbox", "/api/events", "/api/info/**").permitAll() .anyRequest().hasRole("USER") .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) @@ -122,19 +136,6 @@ public class SecurityConfig { return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); } - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("*")); - configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); - configuration.setAllowedHeaders(Collections.singletonList("*")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", configuration); - - return source; - } @Override public void configure(WebSecurity web) { web.debug(false); @@ -182,6 +183,7 @@ public class SecurityConfig { .anyRequest().permitAll() .and() .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and().cors().configurationSource(corsConfigurationSource()) .and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .invalidSessionUrl("/") diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index d1cdac8b..5902220f 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -488,6 +488,11 @@ public class ServerTests { .header("Origin", "http://api.example.net")) .andExpect(status().isOk()) .andExpect(header().string("Access-Control-Allow-Origin", "*")); + mockMvc.perform( + get("/u/ugnich") + .header("Origin", "http://api.example.net")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", "*")); } @Test -- cgit v1.2.3 From ecf4b707f74298618d019bb8108c25f8fc5ad4c6 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 20 Dec 2018 00:36:16 +0300 Subject: getMyFeed optimization --- .../com/juick/service/MessagesServiceImpl.java | 40 ++++++++++------------ .../java/com/juick/server/tests/ServerTests.java | 6 ++-- 2 files changed, 21 insertions(+), 25 deletions(-) (limited to 'src/test/java') diff --git a/src/main/java/com/juick/service/MessagesServiceImpl.java b/src/main/java/com/juick/service/MessagesServiceImpl.java index 4e5dca81..9d9a809d 100644 --- a/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -577,29 +577,25 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ .addValue("before", before); List mids = getNamedParameterJdbcTemplate().queryForList( - "(SELECT message_id FROM messages " + - " INNER JOIN subscr_users ON (subscr_users.suser_id = :uid AND subscr_users.user_id = messages.user_id) " + - " WHERE " + - (before > 0 ? - " message_id < :before AND " : StringUtils.EMPTY) + - " (privacy >= 0 OR (privacy >= -2 AND privacy <= -1" + - " AND EXISTS (SELECT 1 FROM wl_users w WHERE w.wl_user_id = :uid and w.user_id = messages.user_id))) " + - " AND NOT EXISTS (SELECT 1 FROM bl_tags bt WHERE bt.tag_id IN " + - "(SELECT tag_id FROM messages_tags WHERE message_id = messages.message_id) and :uid = bt.user_id))" + - " UNION " + - " (SELECT message_id FROM messages WHERE user_id=:uid " + - (before > 0 ? - " AND message_id < :before " : StringUtils.EMPTY) + + "SELECT message_id FROM messages WHERE " + + "(user_id=:uid OR " + + "(EXISTS (SELECT 1 FROM subscr_users WHERE subscr_users.suser_id = :uid " + + "AND subscr_users.user_id = messages.user_id) " + + "AND NOT EXISTS (SELECT 1 FROM bl_tags bt WHERE bt.tag_id IN " + + "(SELECT tag_id FROM messages_tags WHERE message_id = messages.message_id) AND :uid = bt.user_id) " + + "AND (privacy >= 0 OR (privacy >= -2 AND privacy <= -1 " + + "AND EXISTS (SELECT 1 FROM wl_users w WHERE w.wl_user_id = :uid and w.user_id = messages.user_id)))) " + (recommended ? - ") UNION " + - " (SELECT f.message_id as message_id FROM favorites f INNER JOIN messages ON " + - "f.message_id=messages.message_id WHERE " + - "EXISTS (SELECT 1 FROM subscr_users s WHERE s.suser_id = :uid and f.user_id = s.user_id)" + - (before > 0 ? - " AND f.message_id < :before " : StringUtils.EMPTY) : StringUtils.EMPTY) + - " AND NOT EXISTS (SELECT 1 FROM bl_users b WHERE b.user_id = :uid and b.bl_user_id = messages.user_id)" + - " AND NOT EXISTS (SELECT 1 FROM bl_tags bt WHERE bt.tag_id IN " + - "(SELECT tag_id FROM messages_tags WHERE message_id = messages.message_id) and :uid = bt.user_id)) " + + "OR (EXISTS (SELECT 1 FROM favorites WHERE favorites.message_id=messages.message_id " + + "AND favorites.user_id IN (SELECT user_id FROM subscr_users WHERE suser_id=:uid)) " + + "AND NOT EXISTS (SELECT 1 FROM bl_tags bt WHERE bt.tag_id IN (SELECT tag_id FROM messages_tags " + + "WHERE message_id = messages.message_id) and :uid = bt.user_id) " + + "AND (privacy >= 0 OR (privacy >= -2 AND privacy <= -1 " + + "AND EXISTS (SELECT 1 FROM wl_users w " + + "WHERE w.wl_user_id = :uid and w.user_id = messages.user_id)))) " + + "AND NOT EXISTS (SELECT 1 FROM bl_users b WHERE b.user_id = :uid and b.bl_user_id = messages.user_id)" : StringUtils.EMPTY) + + ") " + + (before > 0 ? "AND message_id < :before " : StringUtils.EMPTY) + "ORDER BY message_id DESC LIMIT 20", sqlParameterSource, Integer.class); diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 5902220f..32afd4f9 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -1041,8 +1041,8 @@ public class ServerTests { tagService.updateTags(newMid, Collections.singletonList(banned)); assertThat(messagesService.getMessage(newMid).get().getTags().size(), is(0)); privacyQueriesService.blacklistUser(freefd, userService.getUserByUID(newUid).orElse(AnonymousUser.INSTANCE)); - assertTrue(messagesService.getMessages(AnonymousUser.INSTANCE, messagesService.getMyFeed(freefd.getUid(), 0, true)) - .stream().noneMatch(m -> m.getMid() == newMid)); + assertTrue(messagesService.getMyFeed(freefd.getUid(), 0, true) + .stream().noneMatch(m -> m == newMid)); } @Test public void tagsShouldBeDeserializedFromXml() throws JAXBException { @@ -1093,7 +1093,7 @@ public class ServerTests { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); - Document doc = db.parse(new ByteArrayInputStream(sw.toString().getBytes(CharEncoding.UTF_8))); + Document doc = db.parse(new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8))); Node juickNode = doc.getDocumentElement(); NamedNodeMap attrs = juickNode.getAttributes(); assertEquals("date should be in ts field", DateFormattersHolder.getMessageFormatterInstance().format(currentDate), -- cgit v1.2.3 From 3af1fa43a25ba844b999d0be6c1139e6a98046fb Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 31 Dec 2018 00:02:02 +0300 Subject: Use external XMPP server --- src/main/java/com/juick/server/XMPPConnection.java | 19 +- src/main/java/com/juick/server/XMPPServer.java | 429 --------------------- .../com/juick/server/configuration/XMPPConfig.java | 11 - .../java/com/juick/server/xmpp/XMPPStatusPage.java | 32 -- .../com/juick/server/xmpp/helpers/XMPPStatus.java | 48 --- .../com/juick/server/xmpp/router/Handshake.java | 39 -- .../java/com/juick/server/xmpp/router/Stream.java | 202 ---------- .../server/xmpp/router/StreamComponentServer.java | 57 --- .../com/juick/server/xmpp/router/StreamError.java | 57 --- .../juick/server/xmpp/router/StreamFeatures.java | 95 ----- .../juick/server/xmpp/router/StreamHandler.java | 13 - .../juick/server/xmpp/router/StreamNamespaces.java | 10 - .../com/juick/server/xmpp/router/XMPPError.java | 73 ---- .../com/juick/server/xmpp/router/XMPPRouter.java | 220 ----------- .../com/juick/server/xmpp/router/XmlUtils.java | 88 ----- .../java/com/juick/server/xmpp/s2s/CacheEntry.java | 40 -- .../java/com/juick/server/xmpp/s2s/Connection.java | 158 -------- .../com/juick/server/xmpp/s2s/ConnectionIn.java | 232 ----------- .../juick/server/xmpp/s2s/ConnectionListener.java | 16 - .../com/juick/server/xmpp/s2s/ConnectionOut.java | 190 --------- .../java/com/juick/server/xmpp/s2s/DNSQueries.java | 65 ---- .../juick/server/xmpp/s2s/util/DialbackUtils.java | 37 -- .../java/com/juick/server/tests/ServerTests.java | 81 ---- 23 files changed, 6 insertions(+), 2206 deletions(-) delete mode 100644 src/main/java/com/juick/server/XMPPServer.java delete mode 100644 src/main/java/com/juick/server/xmpp/XMPPStatusPage.java delete mode 100644 src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/Handshake.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/Stream.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/StreamError.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/StreamFeatures.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/StreamHandler.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/XMPPError.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/XMPPRouter.java delete mode 100644 src/main/java/com/juick/server/xmpp/router/XmlUtils.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/Connection.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java (limited to 'src/test/java') diff --git a/src/main/java/com/juick/server/XMPPConnection.java b/src/main/java/com/juick/server/XMPPConnection.java index dfbe92d5..c4816478 100644 --- a/src/main/java/com/juick/server/XMPPConnection.java +++ b/src/main/java/com/juick/server/XMPPConnection.java @@ -24,7 +24,6 @@ import com.juick.service.component.*; import com.juick.model.CommandResult; import com.juick.server.xmpp.iq.MessageQuery; import com.juick.server.xmpp.s2s.BasicXmppSession; -import com.juick.server.xmpp.s2s.StanzaListener; import com.juick.service.*; import com.juick.util.MessageUtils; import org.apache.commons.codec.digest.DigestUtils; @@ -81,14 +80,12 @@ import java.util.concurrent.ExecutorService; /** * @author ugnich */ -public class XMPPConnection implements StanzaListener, NotificationListener { +public class XMPPConnection implements NotificationListener { private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); private ExternalComponent router; @Inject - private XMPPServer xmpp; - @Inject private CommandsManager commandsManager; @Value("${xmppbot_jid:juick@localhost}") private Jid jid; @@ -96,6 +93,8 @@ public class XMPPConnection implements StanzaListener, NotificationListener { private String componentName; @Value("${component_port:5347}") private int componentPort; + @Value("${component_host:localhost}") + private String componentHost; @Value("${xmpp_password:secret}") private String password; @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") @@ -110,7 +109,7 @@ public class XMPPConnection implements StanzaListener, NotificationListener { @Inject private BasicXmppSession session; @Inject - private ExecutorService service; + private ExecutorService executorService; @Value("${service_user:juick}") private String serviceUsername; @Inject @@ -121,8 +120,7 @@ public class XMPPConnection implements StanzaListener, NotificationListener { @PostConstruct public void init() { logger.info("stream router start connecting to {}", componentPort); - xmpp.addStanzaListener(this); - router = ExternalComponent.create(componentName, password, session.getConfiguration(), "localhost", componentPort); + router = ExternalComponent.create(componentName, password, session.getConfiguration(), componentHost, componentPort); ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class); serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class); @@ -259,7 +257,7 @@ public class XMPPConnection implements StanzaListener, NotificationListener { router.addInboundPresenceListener(event -> { incomingPresence(event.getPresence()); }); - service.submit(() -> { + executorService.submit(() -> { try { router.connect(); } catch (XmppException e) { @@ -644,11 +642,6 @@ public class XMPPConnection implements StanzaListener, NotificationListener { return null; } - @Override - public void stanzaReceived(Stanza xmlValue) { - router.send(xmlValue); - } - private void broadcastPresence(Presence.Type type) { Presence presence = new Presence(); presence.setFrom(jid); diff --git a/src/main/java/com/juick/server/XMPPServer.java b/src/main/java/com/juick/server/XMPPServer.java deleted file mode 100644 index 86ab6a78..00000000 --- a/src/main/java/com/juick/server/XMPPServer.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.server.xmpp.router.StreamError; -import com.juick.server.xmpp.s2s.*; -import com.juick.service.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.xmlpull.v1.XmlPullParserException; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.stanza.model.Stanza; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.inject.Inject; -import javax.net.ssl.*; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; -import java.io.IOException; -import java.io.StringReader; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.SecureRandom; -import java.security.cert.*; -import java.time.Duration; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * @author ugnich - */ -public class XMPPServer implements ConnectionListener { - private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); - - private static final int TIMEOUT_MINUTES = 15; - - @Inject - public ExecutorService service; - @Value("${hostname:localhost}") - private Jid jid; - @Value("${s2s_port:5269}") - private int s2sPort; - @Value("${broken_ssl_hosts:}") - public String[] brokenSSLhosts; - @Value("${banned_hosts:}") - public String[] bannedHosts; - - private final List inConnections = new CopyOnWriteArrayList<>(); - private final Map> outConnections = new ConcurrentHashMap<>(); - private final List outCache = new CopyOnWriteArrayList<>(); - private final List stanzaListeners = new CopyOnWriteArrayList<>(); - private final AtomicBoolean closeFlag = new AtomicBoolean(false); - - SSLContext sc; - CertificateFactory cf; - CertPathValidator cpv; - PKIXParameters params; - private TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - } - }; - private boolean tlsConfigured = false; - - - private ServerSocket listener; - - @Inject - private BasicXmppSession session; - @Inject - private UserService userService; - @Inject - private KeystoreManager keystoreManager; - - @PostConstruct - public void init() throws KeyStoreException { - closeFlag.set(false); - try { - sc = SSLContext.getInstance("TLSv1.2"); - sc.init(keystoreManager.getKeymanagerFactory().getKeyManagers(), trustAllCerts, new SecureRandom()); - TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - Set ca = new HashSet<>(); - trustManagerFactory.init((KeyStore)null); - Arrays.stream(trustManagerFactory.getTrustManagers()).forEach(t -> Arrays.stream(((X509TrustManager)t).getAcceptedIssuers()).forEach(cert -> ca.add(new TrustAnchor(cert, null)))); - params = new PKIXParameters(ca); - params.setRevocationEnabled(false); - cpv = CertPathValidator.getInstance("PKIX"); - cf = CertificateFactory.getInstance( "X.509" ); - tlsConfigured = true; - } catch (Exception e) { - logger.warn("tls unavailable"); - } - service.submit(() -> { - try { - listener = new ServerSocket(s2sPort); - logger.info("s2s listener ready"); - while (!listener.isClosed()) { - if (Thread.currentThread().isInterrupted()) break; - Socket socket = listener.accept(); - ConnectionIn client = new ConnectionIn(this, socket); - addConnectionIn(client); - service.submit(client); - } - } catch (SocketException e) { - // shutdown - } catch (IOException | XmlPullParserException e) { - logger.warn("xmpp exception", e); - } - }); - } - - public void addConnectionIn(ConnectionIn c) { - c.setListener(this); - inConnections.add(c); - } - - public void addConnectionOut(ConnectionOut c, Optional socket) { - c.setListener(this); - outConnections.put(c, socket); - } - - public void removeConnectionIn(ConnectionIn c) { - inConnections.remove(c); - } - - public void removeConnectionOut(ConnectionOut c) { - outConnections.remove(c); - } - - public String getFromCache(Jid to) { - final String[] cache = new String[1]; - outCache.stream().filter(c -> c.hostname != null && c.hostname.equals(to)).findFirst().ifPresent(c -> { - cache[0] = c.xml; - outCache.remove(c); - }); - return cache[0]; - } - - public Optional getConnectionOut(Jid hostname, boolean needReady) { - return outConnections.keySet().stream().filter(c -> c.to != null && - c.to.equals(hostname) && (!needReady || c.streamReady)).findFirst(); - } - - public Optional getConnectionIn(String streamID) { - return inConnections.stream().filter(c -> c.streamID != null && c.streamID.equals(streamID)).findFirst(); - } - - public void sendOut(Jid hostname, String xml) { - boolean haveAnyConn = false; - - ConnectionOut connOut = null; - for (ConnectionOut c : outConnections.keySet()) { - if (c.to != null && c.to.equals(hostname)) { - if (c.streamReady) { - connOut = c; - break; - } else { - haveAnyConn = true; - break; - } - } - } - if (connOut != null) { - connOut.send(xml); - return; - } - - boolean haveCache = false; - for (CacheEntry c : outCache) { - if (c.hostname != null && c.hostname.equals(hostname)) { - c.xml += xml; - c.updated = Instant.now(); - haveCache = true; - break; - } - } - if (!haveCache) { - outCache.add(new CacheEntry(hostname, xml)); - } - - if (!haveAnyConn && !closeFlag.get()) { - try { - createDialbackConnection(hostname.toEscapedString(), null, null); - } catch (Exception e) { - logger.warn("dialback error", e); - } - } - } - - void createDialbackConnection(String to, String checkSID, String dbKey) throws Exception { - ConnectionOut connectionOut = new ConnectionOut(getJid(), Jid.of(to), null, null, checkSID, dbKey); - addConnectionOut(connectionOut, Optional.empty()); - service.submit(() -> { - try { - Socket socket = new Socket(); - socket.connect(DNSQueries.getServerAddress(to)); - connectionOut.setInputStream(socket.getInputStream()); - connectionOut.setOutputStream(socket.getOutputStream()); - addConnectionOut(connectionOut, Optional.of(socket)); - connectionOut.connect(); - } catch (IOException e) { - logger.info("dialback to " + to + " exception", e); - } - }); - } - - public void startDialback(Jid from, String streamId, String dbKey) throws Exception { - Optional c = getConnectionOut(from, false); - if (c.isPresent()) { - c.get().sendDialbackVerify(streamId, dbKey); - } else { - createDialbackConnection(from.toEscapedString(), streamId, dbKey); - } - } - - public void addStanzaListener(StanzaListener listener) { - stanzaListeners.add(listener); - } - - public void onStanzaReceived(String xmlValue) { - logger.info("S2S: {}", xmlValue); - Stanza stanza = parse(xmlValue); - stanzaListeners.forEach(l -> l.stanzaReceived(stanza)); - } - - public BasicXmppSession getSession() { - return session; - } - - public List getInConnections() { - return inConnections; - } - - public Map> getOutConnections() { - return outConnections; - } - - @Override - public boolean isTlsAvailable() { - return tlsConfigured; - } - - @Override - public void starttls(ConnectionIn connection) { - logger.debug("stream {} securing", connection.streamID); - connection.sendStanza(""); - try { - connection.setSocket(sc.getSocketFactory().createSocket(connection.getSocket(), connection.getSocket().getInetAddress().getHostAddress(), - connection.getSocket().getPort(), false)); - SSLSocket sslSocket = (SSLSocket) connection.getSocket(); - sslSocket.addHandshakeCompletedListener(handshakeCompletedEvent -> { - try { - CertPath certPath = cf.generateCertPath(Arrays.asList(handshakeCompletedEvent.getPeerCertificates())); - cpv.validate(certPath, params); - connection.setTrusted(true); - logger.info("connection from {} is trusted", connection.from); - } catch (SSLPeerUnverifiedException | CertificateException | CertPathValidatorException | InvalidAlgorithmParameterException e) { - logger.info("connection from {} is NOT trusted, falling back to dialback", connection.from); - } - }); - sslSocket.setUseClientMode(false); - sslSocket.setNeedClientAuth(true); - sslSocket.startHandshake(); - connection.setSecured(true); - logger.debug("stream from {} secured", connection.streamID); - connection.restartParser(); - } catch (XmlPullParserException | IOException sex) { - logger.warn("stream {} ssl error {}", connection.streamID, sex); - connection.sendStanza(""); - removeConnectionIn(connection); - connection.closeConnection(); - } - } - - @Override - public void proceed(ConnectionOut connection) { - try { - Socket socket = outConnections.get(connection).get(); - socket = sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(), - socket.getPort(), false); - SSLSocket sslSocket = (SSLSocket) socket; - sslSocket.addHandshakeCompletedListener(handshakeCompletedEvent -> { - try { - CertPath certPath = cf.generateCertPath(Arrays.asList(handshakeCompletedEvent.getPeerCertificates())); - cpv.validate(certPath, params); - connection.setTrusted(true); - logger.info("connection to {} is trusted", connection.to); - } catch (SSLPeerUnverifiedException | CertificateException | CertPathValidatorException | InvalidAlgorithmParameterException e) { - logger.info("connection to {} is NOT trusted, falling back to dialback", connection.to); - } - }); - sslSocket.setNeedClientAuth(true); - sslSocket.startHandshake(); - connection.setSecured(true); - logger.debug("stream to {} secured", connection.getStreamID()); - connection.setInputStream(socket.getInputStream()); - connection.setOutputStream(socket.getOutputStream()); - connection.restartStream(); - connection.sendOpenStream(); - } catch (NoSuchElementException | XmlPullParserException | IOException sex) { - logger.error("s2s ssl error: {} {}, error {}", connection.to, connection.getStreamID(), sex); - connection.send(""); - removeConnectionOut(connection); - connection.logoff(); - } - } - - @Override - public void verify(ConnectionOut connection, String from, String type, String sid) { - if (from != null && from.equals(connection.to.toEscapedString()) && sid != null && !sid.isEmpty() && type != null) { - getConnectionIn(sid).ifPresent(c -> c.sendDialbackResult(Jid.of(from), type)); - } - } - - @Override - public void dialbackError(ConnectionOut connection, StreamError error) { - logger.warn("Stream error from {}: {}", connection.getStreamID(), error.getCondition()); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void finished(ConnectionOut connection, boolean dirty) { - logger.warn("stream to {} {} finished, dirty={}", connection.to, connection.getStreamID(), dirty); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void exception(ConnectionOut connection, Exception ex) { - logger.error("s2s out exception: {} {}, exception {}", connection.to, connection.getStreamID(), ex); - removeConnectionOut(connection); - connection.logoff(); - } - - @Override - public void ready(ConnectionOut connection) { - logger.debug("stream to {} {} ready", connection.to, connection.getStreamID()); - String cache = getFromCache(connection.to); - if (cache != null) { - logger.debug("stream to {} {} sending cache", connection.to, connection.getStreamID()); - connection.send(cache); - } - } - - @Override - public boolean securing(ConnectionOut connection) { - return tlsConfigured && !Arrays.asList(brokenSSLhosts).contains(connection.to.toEscapedString()); - } - - public Stanza parse(String xml) { - try { - Unmarshaller unmarshaller = session.createUnmarshaller(); - return (Stanza)unmarshaller.unmarshal(new StringReader(xml)); - } catch (JAXBException e) { - logger.error("JAXB exception", e); - } - return null; - } - - public Jid getJid() { - return jid; - } - @Scheduled(fixedDelay = 10000) - public void cleanUp() { - Instant now = Instant.now(); - outConnections.keySet().stream().filter(c -> Duration.between(c.getUpdated(), now).toMinutes() > TIMEOUT_MINUTES) - .forEach(c -> { - logger.info("closing idle outgoing connection to {}", c.to); - c.logoff(); - outConnections.remove(c); - }); - - inConnections.stream().filter(c -> Duration.between(c.updated, now).toMinutes() > TIMEOUT_MINUTES) - .forEach(c -> { - logger.info("closing idle incoming connection from {}", c.from); - c.closeConnection(); - inConnections.remove(c); - }); - } - @PreDestroy - public void preDestroy() throws IOException { - closeFlag.set(true); - if (listener != null && !listener.isClosed()) { - listener.close(); - } - service.shutdown(); - logger.info("XMPP server destroyed"); - } - - public int getServerPort() { - return s2sPort; - } -} diff --git a/src/main/java/com/juick/server/configuration/XMPPConfig.java b/src/main/java/com/juick/server/configuration/XMPPConfig.java index 2feef286..ffacf4ef 100644 --- a/src/main/java/com/juick/server/configuration/XMPPConfig.java +++ b/src/main/java/com/juick/server/configuration/XMPPConfig.java @@ -1,10 +1,8 @@ package com.juick.server.configuration; import com.juick.server.XMPPConnection; -import com.juick.server.XMPPServer; import com.juick.server.xmpp.JidConverter; import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.server.xmpp.router.XMPPRouter; import com.juick.server.xmpp.s2s.BasicXmppSession; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -40,15 +38,6 @@ public class XMPPConfig { return cs; } @Bean - public XMPPServer xmppServer() { - return new XMPPServer(); - } - @Bean - public XMPPRouter xmppRouter() { - return new XMPPRouter(); - } - @Bean - @DependsOn("xmppRouter") public XMPPConnection xmppConnection() { return new XMPPConnection(); } diff --git a/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java b/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java deleted file mode 100644 index 231696ec..00000000 --- a/src/main/java/com/juick/server/xmpp/XMPPStatusPage.java +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 99d89866..00000000 --- a/src/main/java/com/juick/server/xmpp/helpers/XMPPStatus.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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 inbound; - private List outbound; - - public List getInbound() { - return inbound; - } - - public void setInbound(List inbound) { - this.inbound = inbound; - } - - public List getOutbound() { - return outbound; - } - - public void setOutbound(List outbound) { - this.outbound = outbound; - } -} diff --git a/src/main/java/com/juick/server/xmpp/router/Handshake.java b/src/main/java/com/juick/server/xmpp/router/Handshake.java deleted file mode 100644 index 0bc501dd..00000000 --- a/src/main/java/com/juick/server/xmpp/router/Handshake.java +++ /dev/null @@ -1,39 +0,0 @@ -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("").append(getValue()).append(""); - } 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 deleted file mode 100644 index 2154edf6..00000000 --- a/src/main/java/com/juick/server/xmpp/router/Stream.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Juick - * Copyright (C) 2008-2011, Ugnich Anton - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.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 deleted file mode 100644 index a58adfc5..00000000 --- a/src/main/java/com/juick/server/xmpp/router/StreamComponentServer.java +++ /dev/null @@ -1,57 +0,0 @@ -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("", 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 deleted file mode 100644 index f731f039..00000000 --- a/src/main/java/com/juick/server/xmpp/router/StreamError.java +++ /dev/null @@ -1,57 +0,0 @@ -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("<%s xmlns='%s'/>", 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 deleted file mode 100644 index e8fc324f..00000000 --- a/src/main/java/com/juick/server/xmpp/router/StreamFeatures.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 . - */ -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 deleted file mode 100644 index 048c61ec..00000000 --- a/src/main/java/com/juick/server/xmpp/router/StreamHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 1b9b1965..00000000 --- a/src/main/java/com/juick/server/xmpp/router/StreamNamespaces.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 0cf9a3bc..00000000 --- a/src/main/java/com/juick/server/xmpp/router/XMPPError.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 . - */ -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(""); - } else { - str.append("/>"); - } - str.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 deleted file mode 100644 index 6d67fa9c..00000000 --- a/src/main/java/com/juick/server/xmpp/router/XMPPRouter.java +++ /dev/null @@ -1,220 +0,0 @@ -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 connections = Collections.synchronizedList(new ArrayList<>()); - private final List 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 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 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 deleted file mode 100644 index 7579489f..00000000 --- a/src/main/java/com/juick/server/xmpp/router/XmlUtils.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Juick - * Copyright (C) 2008-2011, Ugnich Anton - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.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(""); - return ret.toString(); - } -} diff --git a/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java b/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java deleted file mode 100644 index 33e875bd..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/CacheEntry.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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 deleted file mode 100644 index 4fa8e741..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/Connection.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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 += ">...\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(""); - } 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 deleted file mode 100644 index 3929a69f..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/ConnectionIn.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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 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"); - Thread.currentThread().setName(String.format("XMPP S2S IN %s - %d", from, Thread.currentThread().getId())); - - 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(""); - logger.debug("stream from {} {} dialback verify valid", vfrom, streamID); - setAuthenticated(true); - } else { - sendStanza(""); - 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(""); - 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 = ""; - if (xmppversionnew) { - openStream += ""; - if (listener != null && listener.isTlsAvailable() && !Arrays.asList(xmpp.brokenSSLhosts).contains(from)) { - if (!isSecured()) { - openStream += ""; - } else if (!isAuthenticated() && isTrusted()) { - openStream += "" + - "EXTERNAL" + - ""; - } - } - openStream += ""; - } - sendStanza(openStream); - } - - public void sendDialbackResult(Jid sfrom, String type) { - sendStanza(""); - 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 deleted file mode 100644 index 4c32b9ae..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/ConnectionListener.java +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 2b919da4..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/ConnectionOut.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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(); - Thread.currentThread().setName(String.format("XMPP S2S OUT %s - %d", to.toEscapedString(), Thread.currentThread().getId())); - } - - public void sendOpenStream() throws IOException { - send(""); - } - - void processDialback() throws Exception { - if (checkSID != null) { - sendDialbackVerify(checkSID, dbKey); - } - send("" + - dbKey + ""); - } - - @Override - public void handshake() { - try { - restartStream(); - - sendOpenStream(); - - parser.next(); // stream:stream - streamID = parser.getAttributeValue(null, "id"); - if (streamID == null || streamID.isEmpty()) { - throw new Exception("stream to " + to + " invalid first packet"); - } - - logger.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(""); - } else if (secured && features.EXTERNAL >=0) { - String authid = Base64.encodeBase64String(from.toEscapedString().getBytes(Charsets.UTF_8)); - send(String.format("%s", 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("" + - key + ""); - } - public void setListener(ConnectionListener listener) { - this.listener = listener; - } - - public String getStreamID() { - return streamID; - } - - public boolean isSecured() { - return secured; - } - - public void setSecured(boolean secured) { - this.secured = secured; - } - - 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 deleted file mode 100644 index 1367d333..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/DNSQueries.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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 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/util/DialbackUtils.java b/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java deleted file mode 100644 index d25dbad8..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/util/DialbackUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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); - } -} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 32afd4f9..620e210c 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -42,8 +42,6 @@ import com.juick.server.api.xnodeinfo2.model.NodeInfo; import com.juick.server.util.HttpUtils; import com.juick.server.util.ImageUtils; import com.juick.server.www.WebApp; -import com.juick.server.xmpp.helpers.XMPPStatus; -import com.juick.server.xmpp.s2s.ConnectionIn; import com.juick.service.*; import com.juick.service.component.MessageEvent; import com.juick.util.DateFormattersHolder; @@ -51,7 +49,6 @@ import com.juick.util.MessageUtils; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.template.PebbleTemplate; -import org.apache.commons.codec.CharEncoding; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.io.IOUtils; @@ -162,8 +159,6 @@ public class ServerTests { @Inject private ObjectMapper jsonMapper; @Inject - private XMPPServer server; - @Inject private CommandsManager commandsManager; @Inject private XMPPConnection router; @@ -687,63 +682,6 @@ public class ServerTests { .andExpect(xpath("/rss/channel/description").string("The latest messages at Juick")); } @Test - public void statusPageIsUp() throws Exception { - mockMvc.perform(get("/api/status").with(httpBasic(ugnichName, ugnichPassword))).andExpect(status().isOk()); - assertThat(server.getJid(), equalTo(jid)); - } - @Test - public void botIsUpAndProcessingResourceConstraints() throws Exception { - jdbcTemplate.execute("DELETE FROM users WHERE nick='renha'"); - int renhaId = userService.createUser("renha", "umnnbt"); - Jid from = Jid.of("renha@serverstorageisfull.tld"); - jdbcTemplate.update("INSERT INTO jids(user_id,jid,active) VALUES(?,?,?)", - renhaId, from.toEscapedString(), 1); - rocks.xmpp.core.stanza.model.Message xmppMessage = new rocks.xmpp.core.stanza.model.Message(); - xmppMessage.setType(rocks.xmpp.core.stanza.model.Message.Type.ERROR); - xmppMessage.setFrom(from); - xmppMessage.setTo(botJid); - StanzaError err = new StanzaError(StanzaError.Type.CANCEL, Condition.RESOURCE_CONSTRAINT); - xmppMessage.setError(err); - Function isActive = f -> jdbcTemplate.queryForObject("SELECT active FROM jids WHERE user_id=?", Integer.class, f) == 1; - assertThat(isActive.apply(renhaId), equalTo(true)); - ClientMessage result = router.incomingMessage(xmppMessage); - assertNull(result); - assertThat(isActive.apply(renhaId), equalTo(false)); - xmppMessage.setError(null); - xmppMessage.setType(rocks.xmpp.core.stanza.model.Message.Type.CHAT); - xmppMessage.setBody("On"); - result = router.incomingMessage(xmppMessage); - assertThat(result.getBody(), equalTo("XMPP notifications are activated")); - assertTrue(isActive.apply(renhaId)); - xmppMessage.setBody("*test test"); - result = router.incomingMessage(xmppMessage); - assertThat(result.getBody(), startsWith("New message posted")); - xmppMessage.setFrom(from); - xmppMessage.setBody("PING"); - result = router.incomingMessage(xmppMessage); - assertThat(result.getBody(), equalTo("PONG")); - int secretlySadId = userService.createUser("secretlysad", "bbk"); - xmppMessage.setTo(botJid.withLocal("secretlysad")); - xmppMessage.setBody("What's up?"); - result = router.incomingMessage(xmppMessage); - assertThat(result, is(nullValue())); - xmppMessage.setTo(botJid); - xmppMessage.setBody("@secretlysad Hey!"); - result = router.incomingMessage(xmppMessage); - assertThat(result.getBody(), is("Private message sent")); - assertThat(pmQueriesService.getPMMessages(renhaId, secretlySadId).size(), is(2)); - String xml = "@yo:\n" + - "343432434\n" + - "\n" + - "#2 http://juick.com/2juick-2343432434@yo"; - result = router.incomingMessage((rocks.xmpp.core.stanza.model.Message)server.parse(xml)); - String active = ""; - result = router.incomingMessage((rocks.xmpp.core.stanza.model.Message)server.parse(active)); - xmppMessage.setFrom(botJid); - // TODO: assert events - } - @Test public void botCommandsTests() throws Exception { assertThat(commandsManager.processCommand(AnonymousUser.INSTANCE, "PING", emptyUri).getText(), is("PONG")); // subscription commands have two lines, others have 1 @@ -1328,25 +1266,6 @@ public class ServerTests { assertThat(subscriptionService.getUsersSubscribedToComments(msg, reply).size(), is(0)); } @Test - public void xmppStatusApi() throws Exception { - Supplier getStatus = () -> { - try { - MvcResult result = mockMvc.perform(get("/api/xmpp-status").with(httpBasic(ugnichName, ugnichPassword))) - .andExpect(status().isOk()).andReturn(); - return jsonMapper.readValue(result.getResponse().getContentAsString(), XMPPStatus.class); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - }; - assertThat(getStatus.get().getInbound(), is(nullValue())); - ConnectionIn test = new ConnectionIn(server, new Socket("localhost", server.getServerPort())); - test.from.add(Jid.of("test")); - server.getInConnections().clear(); - server.addConnectionIn(test); - assertThat(getStatus.get().getInbound().size(), is(1)); - } - @Test public void credentialsShouldNeverBeSerialized() throws Exception { int uid = userService.createUser("yyy", "xxxx"); User yyy = userService.getUserByUID(uid).get(); -- cgit v1.2.3 From dcf1b9e5e4312cfd685c1f3cb0f819e7ac64be75 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 31 Dec 2018 01:35:11 +0300 Subject: XMPP cleanup --- src/main/java/com/juick/server/XMPPConnection.java | 672 -------------------- src/main/java/com/juick/server/XMPPManager.java | 678 +++++++++++++++++++++ .../com/juick/server/configuration/XMPPConfig.java | 23 +- .../juick/server/xmpp/s2s/BasicXmppSession.java | 68 --- .../com/juick/server/xmpp/s2s/StanzaListener.java | 28 - .../java/com/juick/server/tests/ServerTests.java | 8 - 6 files changed, 681 insertions(+), 796 deletions(-) delete mode 100644 src/main/java/com/juick/server/XMPPConnection.java create mode 100644 src/main/java/com/juick/server/XMPPManager.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java delete mode 100644 src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java (limited to 'src/test/java') diff --git a/src/main/java/com/juick/server/XMPPConnection.java b/src/main/java/com/juick/server/XMPPConnection.java deleted file mode 100644 index c4816478..00000000 --- a/src/main/java/com/juick/server/XMPPConnection.java +++ /dev/null @@ -1,672 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server; - -import com.juick.User; -import com.juick.formatters.PlainTextFormatter; -import com.juick.server.www.WebApp; -import com.juick.service.component.*; -import com.juick.model.CommandResult; -import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.server.xmpp.s2s.BasicXmppSession; -import com.juick.service.*; -import com.juick.util.MessageUtils; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import rocks.xmpp.addr.Jid; -import rocks.xmpp.core.XmppException; -import rocks.xmpp.core.session.XmppSession; -import rocks.xmpp.core.stanza.AbstractIQHandler; -import rocks.xmpp.core.stanza.model.*; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.client.ClientPresence; -import rocks.xmpp.core.stanza.model.errors.Condition; -import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; -import rocks.xmpp.extensions.component.accept.ExternalComponent; -import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; -import rocks.xmpp.extensions.disco.model.info.Identity; -import rocks.xmpp.extensions.filetransfer.FileTransfer; -import rocks.xmpp.extensions.filetransfer.FileTransferManager; -import rocks.xmpp.extensions.nick.model.Nickname; -import rocks.xmpp.extensions.oob.model.x.OobX; -import rocks.xmpp.extensions.ping.PingManager; -import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; -import rocks.xmpp.extensions.vcard.temp.model.VCard; -import rocks.xmpp.extensions.version.SoftwareVersionManager; -import rocks.xmpp.extensions.version.model.SoftwareVersion; -import rocks.xmpp.util.XmppUtils; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.inject.Inject; -import javax.xml.bind.JAXBException; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import java.io.IOException; -import java.io.StringWriter; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -/** - * @author ugnich - */ -public class XMPPConnection implements NotificationListener { - - private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); - - private ExternalComponent router; - @Inject - private CommandsManager commandsManager; - @Value("${xmppbot_jid:juick@localhost}") - private Jid jid; - @Value("${hostname:localhost}") - private String componentName; - @Value("${component_port:5347}") - private int componentPort; - @Value("${component_host:localhost}") - private String componentHost; - @Value("${xmpp_password:secret}") - private String password; - @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") - private String tmpDir; - - @Inject - private MessagesService messagesService; - @Inject - private UserService userService; - @Inject - private PMQueriesService pmQueriesService; - @Inject - private BasicXmppSession session; - @Inject - private ExecutorService executorService; - @Value("${service_user:juick}") - private String serviceUsername; - @Inject - private WebApp webApp; - - private User serviceUser; - - @PostConstruct - public void init() { - logger.info("stream router start connecting to {}", componentPort); - router = ExternalComponent.create(componentName, password, session.getConfiguration(), componentHost, componentPort); - ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class); - serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); - EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class); - entityCapabilitiesManager.setNode("https://juick.com/caps"); - MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = router.getManager(MessageDeliveryReceiptsManager.class); - messageDeliveryReceiptsManager.setEnabled(true); - PingManager pingManager = router.getManager(PingManager.class); - pingManager.setEnabled(true); - SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); - softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", - System.getProperty("os.name", "generic"))); - VCard vCard = new VCard(); - vCard.setFormattedName("Juick"); - vCard.setBirthday(LocalDate.of(2008, 10, 22)); - try { - vCard.setUrl(new URL("http://juick.com/")); - vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray( - getClass().getClassLoader().getResource("juick.png")))); - } catch (MalformedURLException e) { - logger.error("invalid url", e); - } catch (IOException e) { - logger.warn("invalid resource", e); - } - router.addIQHandler(MessageQuery.class, iq -> { - Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); - warningMessage.setFrom(jid); - warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); - router.send(warningMessage); - return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); - }); - router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { - @Override - protected IQ processRequest(IQ iq) { - if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) - || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { - return iq.createResult(vCard); - } - User user = userService.getUserByName(iq.getTo().getLocal()); - if (!user.isAnonymous()) { - User info = userService.getUserInfo(user); - VCard userVCard = new VCard(); - userVCard.setFormattedName(info.getFullName()); - userVCard.setNickname(user.getName()); - try { - userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); - if (info.getUrl() != null) { - userVCard.setUrl(new URL(info.getUrl())); - } - } catch (MalformedURLException e) { - logger.warn("url exception", e); - } - return iq.createResult(userVCard); - } - return iq.createError(Condition.BAD_REQUEST); - } - }); - router.addInboundMessageListener(e -> { - ClientMessage result = incomingMessage(e.getMessage()); - if (result != null) { - router.send(result); - } - }); - router.addInboundIQListener(e -> { - IQ iq = e.getIQ(); - Jid jid = iq.getTo(); - if (!jid.getDomain().equals(this.jid.getDomain())) { - router.send(iq); - } - }); - FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); - fileTransferManager.addFileTransferOfferListener(e -> { - try { - List allowedTypes = new ArrayList() {{ - add("png"); - add("jpg"); - }}; - String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); - String targetFilename = String.format("%s.%s", - DigestUtils.md5Hex(String.format("%s-%s", - e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); - if (allowedTypes.contains(attachmentExtension)) { - Path filePath = Paths.get(tmpDir, targetFilename); - FileTransfer ft = e.accept(filePath).get(); - ft.addFileTransferStatusListener(st -> { - logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); - if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { - logger.info("transfer completed"); - try { - Jid initiator = e.getInitiator(); - ClientMessage result = incomingMessageJuick( - userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, - jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); - if (result != null) { - router.send(result); - } - } catch (Exception e1) { - logger.error("ft error", e1); - } - - } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { - logger.info("transfer failed", ft.getException()); - Message msg = new Message(); - msg.setType(Message.Type.CHAT); - msg.setFrom(jid); - msg.setTo(e.getInitiator()); - msg.setBody("File transfer failed, please report to us"); - router.sendMessage(msg); - } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { - logger.info("transfer cancelled"); - } - }); - ft.transfer(); - logger.info("transfer started"); - } else { - e.reject(); - logger.info("transfer rejected"); - } - } catch (IOException | InterruptedException | ExecutionException e1) { - logger.error("ft error", e1); - } - }); - router.addConnectionListener(event -> { - if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { - logger.info("component connected"); - } - }); - router.addSessionStatusListener(event -> { - logger.info("event: " + event.getStatus(), event.getThrowable()); - if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { - logger.info("Authenticated, broadcasting..."); - broadcastPresence(null); - } - }); - router.addInboundPresenceListener(event -> { - incomingPresence(event.getPresence()); - }); - executorService.submit(() -> { - try { - router.connect(); - } catch (XmppException e) { - logger.warn("xmpp exception", e); - } - }); - serviceUser = userService.getUserByName(serviceUsername); - } - - private String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { - StringWriter stanzaWriter = new StringWriter(); - XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( - router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); - router.createMarshaller().marshal(stanza, xmppStreamWriter); - xmppStreamWriter.flush(); - xmppStreamWriter.close(); - return stanzaWriter.toString(); - } - - private void sendJuickMessage(com.juick.Message jmsg, List users) { - List jids = new ArrayList<>(); - - for (User user : users) { - jids.addAll(userService.getJIDsbyUID(user.getUid())); - } - com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - for (String jid : jids) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - - public void sendJuickComment(com.juick.Message jmsg, List users) { - String replyQuote; - String replyTo; - - com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) - : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); - replyTo = replyMessage.getUser().getName(); - com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); - replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); - - String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; - String attachmentUrl = MessageUtils.attachmentUrl(fullReply); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.addExtension(jmsg); - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processMessageEvent(MessageEvent event) { - com.juick.Message msg = event.getMessage(); - List subscribers = event.getUsers(); - if (msg.isService()) { - return; - } - if (MessageUtils.isPM(msg)) { - userService.getJIDsbyUID(msg.getTo().getUid()) - .forEach(userJid -> { - Message mm = new Message(); - mm.setTo(Jid.of(userJid)); - mm.setType(Message.Type.CHAT); - boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); - if (inroster) { - mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); - mm.setBody(msg.getText()); - } else { - mm.setFrom(jid); - mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); - } - router.send(ClientMessage.from(mm)); - }); - } else if (MessageUtils.isReply(msg)) { - sendJuickComment(msg, subscribers); - } - else { - sendJuickMessage(msg, subscribers); - } - } - - private ClientMessage makeReply(Jid jidTo, String txt) { - Message reply = new Message(); - reply.setFrom(jid); - reply.setTo(jidTo); - reply.setType(Message.Type.CHAT); - reply.setBody(txt); - return ClientMessage.from(reply); - } - - @Override - public void processSubscribeEvent(SubscribeEvent subscribeEvent) { - - } - - @Override - public void processLikeEvent(LikeEvent likeEvent) { - List users = likeEvent.getSubscribers(); - com.juick.Message jmsg = likeEvent.getMessage(); - User liker = likeEvent.getUser(); - - if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { - userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { - Message xmppMessage = new Message(); - xmppMessage.setFrom(jid); - xmppMessage.setTo(Jid.of(authorJid)); - xmppMessage.setType(Message.Type.CHAT); - xmppMessage.addExtension(jmsg); - xmppMessage.setBody(String.format("%s recommended your post #%d. %s", - liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); - router.send(ClientMessage.from(xmppMessage)); - }); - } - - String txt = "Recommended by @" + liker.getName() + ":\n"; - txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; - String attachmentUrl = MessageUtils.attachmentUrl(jmsg); - if (StringUtils.isNotEmpty(attachmentUrl)) { - txt += attachmentUrl + "\n"; - } - txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; - txt += "#" + jmsg.getMid(); - if (jmsg.getReplies() > 0) { - if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { - txt += " (" + jmsg.getReplies() + " reply)"; - } else { - txt += " (" + jmsg.getReplies() + " replies)"; - } - } - txt += " http://juick.com/m/" + jmsg.getMid(); - - Nickname nick = new Nickname("@" + jmsg.getUser().getName()); - - Message msg = new Message(); - msg.setFrom(jid); - msg.setBody(txt); - msg.setType(Message.Type.CHAT); - msg.setThread("juick-" + jmsg.getMid()); - msg.addExtension(jmsg); - msg.addExtension(nick); - if (StringUtils.isNotEmpty(attachmentUrl)) { - try { - OobX oob = new OobX(new URI(attachmentUrl)); - msg.addExtension(oob); - } catch (URISyntaxException e) { - logger.warn("uri exception", e); - } - } - - for (User user : users) { - for (String jid : userService.getJIDsbyUID(user.getUid())) { - msg.setTo(Jid.of(jid)); - router.send(ClientMessage.from(msg)); - } - } - } - - @Override - public void processPingEvent(PingEvent pingEvent) { - userService.getJIDsbyUID(pingEvent.getPinger().getUid()) - .forEach(userJid -> { - Presence p = new Presence(Jid.of(userJid)); - p.setFrom(jid); - p.setPriority((byte) 10); - router.send(ClientPresence.from(p)); - }); - } - - @Override - public void processMessageReadEvent(MessageReadEvent messageReadEvent) { - - } - - @Override - public void processTopEvent(TopEvent topEvent) { - com.juick.Message message = topEvent.getMessage(); - try { - commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); - } catch (Exception e) { - logger.warn("XMPP error", e); - } - } - - private void incomingPresence(Presence p) { - final String username = p.getTo().getLocal(); - final boolean toJuick = username.equals(jid.getLocal()); - - if (p.getType() == null) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().asBareJid()); - reply.setTo(p.getFrom().asBareJid()); - reply.setType(Presence.Type.UNSUBSCRIBE); - router.send(ClientPresence.from(reply)); - } else if (p.getType().equals(Presence.Type.PROBE)) { - int uid_to = 0; - if (!toJuick) { - uid_to = userService.getUIDbyName(username); - } else { - User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); - if (visitor != null) { - userService.updateLastSeen(visitor); - } - } - - if (toJuick || uid_to > 0) { - Presence reply = new Presence(); - reply.setFrom(p.getTo().withResource(jid.getResource())); - reply.setTo(p.getFrom()); - reply.setPriority((byte)10); - if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { - reply.setStatus("Send ON to enable notifications"); - } - router.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - router.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { - boolean canSubscribe = false; - if (toJuick) { - canSubscribe = true; - } else { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - canSubscribe = true; - } - } - if (canSubscribe) { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.SUBSCRIBED); - router.send(ClientPresence.from(reply)); - - reply.setFrom(reply.getFrom().withResource(jid.getResource())); - reply.setPriority((byte) 10); - reply.setType(null); - router.send(ClientPresence.from(reply)); - } else { - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.ERROR); - reply.setId(p.getId()); - reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); - router.send(ClientPresence.from(reply)); - } - } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { - if (!toJuick) { - int uid_to = userService.getUIDbyName(username); - if (uid_to > 0) { - pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); - } - } - - Presence reply = new Presence(); - reply.setFrom(p.getTo()); - reply.setTo(p.getFrom()); - reply.setType(Presence.Type.UNSUBSCRIBED); - router.send(ClientPresence.from(reply)); - } - } - - public ClientMessage incomingMessage(Message msg) { - ClientMessage result = null; - if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { - StanzaError error = msg.getError(); - if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { - // offline query is full, deactivating this jid - if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { - logger.info("{} is inactive now", msg.getFrom()); - return null; - } - } - return null; - } - Jid to = msg.getTo(); - if (to.getDomain().equals(router.getDomain().toEscapedString()) || to.equals(this.jid)) { - User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); - if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { - String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); - return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); - } - URI attachment = URI.create(StringUtils.EMPTY); - OobX oobX = msg.getExtension(OobX.class); - if (oobX != null) { - attachment = oobX.getUri(); - } - try { - return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); - } catch (Exception e1) { - logger.warn("message exception", e1); - } - } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain()) - || to.getDomain().endsWith("." + jid.getDomain()))) { - if (logger.isInfoEnabled()) { - try { - logger.info("unhandled message: {}", stanzaToString(msg)); - } catch (JAXBException | XMLStreamException ex) { - logger.error("JAXB exception", ex); - } - } - } else { - return ClientMessage.from(msg); - } - return result; - } - private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { - if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { - return null; - } - - messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); - - int commandlen = command.length(); - - // COMPATIBILITY - if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { - command = command.substring(3); - } - - if (!jid.getLocal().equals(to)) { - // PM - if (!StringUtils.isEmpty(command)) { - commandsManager.commandPM(user_from, null, to, command); - return null; - } - } - - try { - CommandResult result = commandsManager.processCommand(user_from, command, attachment); - if (StringUtils.isNotBlank(result.getText())) { - return makeReply(from, result.getText()); - } - } catch (Exception e) { - logger.warn("xmpp command exception", e); - return makeReply(from, "Error processing command"); - } - return null; - } - - private void broadcastPresence(Presence.Type type) { - Presence presence = new Presence(); - presence.setFrom(jid); - if (type != null) { - presence.setType(type); - } - userService.getActiveJIDs().forEach(j -> { - try { - presence.setTo(Jid.of(j)); - router.send(ClientPresence.from(presence)); - } catch (IllegalArgumentException ex) { - logger.warn("Invalid jid: {}", j, ex); - } - }); - } - - @PreDestroy - public void close() throws Exception { - broadcastPresence(Presence.Type.UNAVAILABLE); - if (router != null) { - router.close(); - } - } - - public ExternalComponent getRouter() { - return router; - } -} diff --git a/src/main/java/com/juick/server/XMPPManager.java b/src/main/java/com/juick/server/XMPPManager.java new file mode 100644 index 00000000..fb8f9711 --- /dev/null +++ b/src/main/java/com/juick/server/XMPPManager.java @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server; + +import com.juick.User; +import com.juick.formatters.PlainTextFormatter; +import com.juick.server.www.WebApp; +import com.juick.service.component.*; +import com.juick.model.CommandResult; +import com.juick.server.xmpp.iq.MessageQuery; +import com.juick.service.*; +import com.juick.util.MessageUtils; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.XmppException; +import rocks.xmpp.core.session.Extension; +import rocks.xmpp.core.session.XmppSession; +import rocks.xmpp.core.session.XmppSessionConfiguration; +import rocks.xmpp.core.session.debug.LogbackDebugger; +import rocks.xmpp.core.stanza.AbstractIQHandler; +import rocks.xmpp.core.stanza.model.*; +import rocks.xmpp.core.stanza.model.client.ClientMessage; +import rocks.xmpp.core.stanza.model.client.ClientPresence; +import rocks.xmpp.core.stanza.model.errors.Condition; +import rocks.xmpp.extensions.caps.EntityCapabilitiesManager; +import rocks.xmpp.extensions.component.accept.ExternalComponent; +import rocks.xmpp.extensions.disco.ServiceDiscoveryManager; +import rocks.xmpp.extensions.disco.model.info.Identity; +import rocks.xmpp.extensions.filetransfer.FileTransfer; +import rocks.xmpp.extensions.filetransfer.FileTransferManager; +import rocks.xmpp.extensions.nick.model.Nickname; +import rocks.xmpp.extensions.oob.model.x.OobX; +import rocks.xmpp.extensions.ping.PingManager; +import rocks.xmpp.extensions.receipts.MessageDeliveryReceiptsManager; +import rocks.xmpp.extensions.vcard.temp.model.VCard; +import rocks.xmpp.extensions.version.SoftwareVersionManager; +import rocks.xmpp.extensions.version.model.SoftwareVersion; +import rocks.xmpp.util.XmppUtils; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +/** + * @author ugnich + */ +public class XMPPManager implements NotificationListener { + + private static final Logger logger = LoggerFactory.getLogger("com.juick.server.xmpp"); + + private ExternalComponent router; + @Inject + private CommandsManager commandsManager; + @Value("${xmppbot_jid:juick@localhost}") + private Jid jid; + @Value("${hostname:localhost}") + private String componentName; + @Value("${component_port:5347}") + private int componentPort; + @Value("${component_host:localhost}") + private String componentHost; + @Value("${xmpp_password:secret}") + private String password; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private ExecutorService executorService; + @Value("${service_user:juick}") + private String serviceUsername; + @Inject + private WebApp webApp; + + private User serviceUser; + + @PostConstruct + public void init() { + logger.info("stream router start connecting to {}", componentPort); + XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() + .extensions(Extension.of(com.juick.Message.class), Extension.of(MessageQuery.class)) + .debugger(LogbackDebugger.class) + .defaultResponseTimeout(Duration.ofMillis(120000)) + .build(); + router = ExternalComponent.create(componentName, password, configuration, componentHost, componentPort); + ServiceDiscoveryManager serviceDiscoveryManager = router.getManager(ServiceDiscoveryManager.class); + serviceDiscoveryManager.addIdentity(Identity.clientBot().withName("Juick")); + EntityCapabilitiesManager entityCapabilitiesManager = router.getManager(EntityCapabilitiesManager.class); + entityCapabilitiesManager.setNode("https://juick.com/caps"); + MessageDeliveryReceiptsManager messageDeliveryReceiptsManager = router.getManager(MessageDeliveryReceiptsManager.class); + messageDeliveryReceiptsManager.setEnabled(true); + PingManager pingManager = router.getManager(PingManager.class); + pingManager.setEnabled(true); + SoftwareVersionManager softwareVersionManager = router.getManager(SoftwareVersionManager.class); + softwareVersionManager.setSoftwareVersion(new SoftwareVersion("Juick", "2.x", + System.getProperty("os.name", "generic"))); + VCard vCard = new VCard(); + vCard.setFormattedName("Juick"); + vCard.setBirthday(LocalDate.of(2008, 10, 22)); + try { + vCard.setUrl(new URL("http://juick.com/")); + vCard.setPhoto(new VCard.Image("image/png", IOUtils.toByteArray( + getClass().getClassLoader().getResource("juick.png")))); + } catch (MalformedURLException e) { + logger.error("invalid url", e); + } catch (IOException e) { + logger.warn("invalid resource", e); + } + router.addIQHandler(MessageQuery.class, iq -> { + Message warningMessage = new Message(iq.getFrom(), Message.Type.CHAT); + warningMessage.setFrom(jid); + warningMessage.setBody("Your XMPP client constantly polls us with XMPP query which is unsupported for years, please find http://juick.com/query#messages in your client code and remove that"); + router.send(warningMessage); + return iq.createError(new StanzaError(Condition.BAD_REQUEST, "Please stop this spam")); + }); + router.addIQHandler(VCard.class, new AbstractIQHandler(IQ.Type.GET) { + @Override + protected IQ processRequest(IQ iq) { + if (iq.getTo().equals(jid) || iq.getTo().asBareJid().equals(jid.asBareJid()) + || iq.getTo().asBareJid().toEscapedString().equals(jid.getDomain())) { + return iq.createResult(vCard); + } + User user = userService.getUserByName(iq.getTo().getLocal()); + if (!user.isAnonymous()) { + User info = userService.getUserInfo(user); + VCard userVCard = new VCard(); + userVCard.setFormattedName(info.getFullName()); + userVCard.setNickname(user.getName()); + try { + userVCard.setPhoto(new VCard.Image(URI.create(webApp.getAvatarUrl(user)))); + if (info.getUrl() != null) { + userVCard.setUrl(new URL(info.getUrl())); + } + } catch (MalformedURLException e) { + logger.warn("url exception", e); + } + return iq.createResult(userVCard); + } + return iq.createError(Condition.BAD_REQUEST); + } + }); + router.addInboundMessageListener(e -> { + ClientMessage result = incomingMessage(e.getMessage()); + if (result != null) { + router.send(result); + } + }); + router.addInboundIQListener(e -> { + IQ iq = e.getIQ(); + Jid jid = iq.getTo(); + if (!jid.getDomain().equals(this.jid.getDomain())) { + router.send(iq); + } + }); + FileTransferManager fileTransferManager = router.getManager(FileTransferManager.class); + fileTransferManager.addFileTransferOfferListener(e -> { + try { + List allowedTypes = new ArrayList() {{ + add("png"); + add("jpg"); + }}; + String attachmentExtension = FilenameUtils.getExtension(e.getName()).toLowerCase(); + String targetFilename = String.format("%s.%s", + DigestUtils.md5Hex(String.format("%s-%s", + e.getInitiator().toString(), e.getSessionId()).getBytes()), attachmentExtension); + if (allowedTypes.contains(attachmentExtension)) { + Path filePath = Paths.get(tmpDir, targetFilename); + FileTransfer ft = e.accept(filePath).get(); + ft.addFileTransferStatusListener(st -> { + logger.debug("{}: received {} of {}", e.getName(), st.getBytesTransferred(), e.getSize()); + if (st.getStatus().equals(FileTransfer.Status.COMPLETED)) { + logger.info("transfer completed"); + try { + Jid initiator = e.getInitiator(); + ClientMessage result = incomingMessageJuick( + userService.getUserByJID(initiator.asBareJid().toEscapedString()), initiator, + jid.getLocal(), StringUtils.defaultString(e.getDescription()).trim(), URI.create(String.format("juick://%s", targetFilename))); + if (result != null) { + router.send(result); + } + } catch (Exception e1) { + logger.error("ft error", e1); + } + + } else if (st.getStatus().equals(FileTransfer.Status.FAILED)) { + logger.info("transfer failed", ft.getException()); + Message msg = new Message(); + msg.setType(Message.Type.CHAT); + msg.setFrom(jid); + msg.setTo(e.getInitiator()); + msg.setBody("File transfer failed, please report to us"); + router.sendMessage(msg); + } else if (st.getStatus().equals(FileTransfer.Status.CANCELED)) { + logger.info("transfer cancelled"); + } + }); + ft.transfer(); + logger.info("transfer started"); + } else { + e.reject(); + logger.info("transfer rejected"); + } + } catch (IOException | InterruptedException | ExecutionException e1) { + logger.error("ft error", e1); + } + }); + router.addConnectionListener(event -> { + if (event.getType().equals(rocks.xmpp.core.session.ConnectionEvent.Type.RECONNECTION_SUCCEEDED)) { + logger.info("component connected"); + } + }); + router.addSessionStatusListener(event -> { + logger.info("event: " + event.getStatus(), event.getThrowable()); + if (event.getStatus().equals(XmppSession.Status.AUTHENTICATED)) { + logger.info("Authenticated, broadcasting..."); + broadcastPresence(null); + } + }); + router.addInboundPresenceListener(event -> { + incomingPresence(event.getPresence()); + }); + executorService.submit(() -> { + try { + router.connect(); + } catch (XmppException e) { + logger.warn("xmpp exception", e); + } + }); + serviceUser = userService.getUserByName(serviceUsername); + } + + private String stanzaToString(Stanza stanza) throws XMLStreamException, JAXBException { + StringWriter stanzaWriter = new StringWriter(); + XMLStreamWriter xmppStreamWriter = XmppUtils.createXmppStreamWriter( + router.getConfiguration().getXmlOutputFactory().createXMLStreamWriter(stanzaWriter)); + router.createMarshaller().marshal(stanza, xmppStreamWriter); + xmppStreamWriter.flush(); + xmppStreamWriter.close(); + return stanzaWriter.toString(); + } + + private void sendJuickMessage(com.juick.Message jmsg, List users) { + List jids = new ArrayList<>(); + + for (User user : users) { + jids.addAll(userService.getJIDsbyUID(user.getUid())); + } + com.juick.Message fullMsg = messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + String txt = "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(fullMsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(fullMsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid() + " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + for (String jid : jids) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + + public void sendJuickComment(com.juick.Message jmsg, List users) { + String replyQuote; + String replyTo; + + com.juick.Message replyMessage = jmsg.getReplyto() > 0 ? messagesService.getReply(jmsg.getMid(), jmsg.getReplyto()) + : messagesService.getMessage(jmsg.getMid()).orElseThrow(IllegalStateException::new); + replyTo = replyMessage.getUser().getName(); + com.juick.Message fullReply = messagesService.getReply(jmsg.getMid(), jmsg.getRid()); + replyQuote = StringUtils.defaultString(fullReply.getReplyQuote()); + + String txt = "Reply by @" + jmsg.getUser().getName() + ":\n" + replyQuote + "\n@" + replyTo + " "; + String attachmentUrl = MessageUtils.attachmentUrl(fullReply); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n" + "#" + jmsg.getMid() + "/" + jmsg.getRid() + " http://juick.com/m/" + jmsg.getMid() + "#" + jmsg.getRid(); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.addExtension(jmsg); + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processMessageEvent(MessageEvent event) { + com.juick.Message msg = event.getMessage(); + List subscribers = event.getUsers(); + if (msg.isService()) { + return; + } + if (MessageUtils.isPM(msg)) { + userService.getJIDsbyUID(msg.getTo().getUid()) + .forEach(userJid -> { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + boolean inroster = pmQueriesService.havePMinRoster(msg.getUser().getUid(), userJid); + if (inroster) { + mm.setFrom(Jid.of(msg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(msg.getText()); + } else { + mm.setFrom(jid); + mm.setBody("Private message from @" + msg.getUser().getName() + ":\n" + msg.getText()); + } + router.send(ClientMessage.from(mm)); + }); + } else if (MessageUtils.isReply(msg)) { + sendJuickComment(msg, subscribers); + } + else { + sendJuickMessage(msg, subscribers); + } + } + + private ClientMessage makeReply(Jid jidTo, String txt) { + Message reply = new Message(); + reply.setFrom(jid); + reply.setTo(jidTo); + reply.setType(Message.Type.CHAT); + reply.setBody(txt); + return ClientMessage.from(reply); + } + + @Override + public void processSubscribeEvent(SubscribeEvent subscribeEvent) { + + } + + @Override + public void processLikeEvent(LikeEvent likeEvent) { + List users = likeEvent.getSubscribers(); + com.juick.Message jmsg = likeEvent.getMessage(); + User liker = likeEvent.getUser(); + + if (!userService.isInBLAny(jmsg.getUser().getUid(), liker.getUid())) { + userService.getJIDsbyUID(jmsg.getUser().getUid()).forEach(authorJid -> { + Message xmppMessage = new Message(); + xmppMessage.setFrom(jid); + xmppMessage.setTo(Jid.of(authorJid)); + xmppMessage.setType(Message.Type.CHAT); + xmppMessage.addExtension(jmsg); + xmppMessage.setBody(String.format("%s recommended your post #%d. %s", + liker.getName(), jmsg.getMid(), PlainTextFormatter.formatUrl(jmsg))); + router.send(ClientMessage.from(xmppMessage)); + }); + } + + String txt = "Recommended by @" + liker.getName() + ":\n"; + txt += "@" + jmsg.getUser().getName() + ":" + MessageUtils.getTagsString(jmsg) + "\n"; + String attachmentUrl = MessageUtils.attachmentUrl(jmsg); + if (StringUtils.isNotEmpty(attachmentUrl)) { + txt += attachmentUrl + "\n"; + } + txt += StringUtils.defaultString(jmsg.getText()) + "\n\n"; + txt += "#" + jmsg.getMid(); + if (jmsg.getReplies() > 0) { + if (jmsg.getReplies() % 10 == 1 && jmsg.getReplies() % 100 != 11) { + txt += " (" + jmsg.getReplies() + " reply)"; + } else { + txt += " (" + jmsg.getReplies() + " replies)"; + } + } + txt += " http://juick.com/m/" + jmsg.getMid(); + + Nickname nick = new Nickname("@" + jmsg.getUser().getName()); + + Message msg = new Message(); + msg.setFrom(jid); + msg.setBody(txt); + msg.setType(Message.Type.CHAT); + msg.setThread("juick-" + jmsg.getMid()); + msg.addExtension(jmsg); + msg.addExtension(nick); + if (StringUtils.isNotEmpty(attachmentUrl)) { + try { + OobX oob = new OobX(new URI(attachmentUrl)); + msg.addExtension(oob); + } catch (URISyntaxException e) { + logger.warn("uri exception", e); + } + } + + for (User user : users) { + for (String jid : userService.getJIDsbyUID(user.getUid())) { + msg.setTo(Jid.of(jid)); + router.send(ClientMessage.from(msg)); + } + } + } + + @Override + public void processPingEvent(PingEvent pingEvent) { + userService.getJIDsbyUID(pingEvent.getPinger().getUid()) + .forEach(userJid -> { + Presence p = new Presence(Jid.of(userJid)); + p.setFrom(jid); + p.setPriority((byte) 10); + router.send(ClientPresence.from(p)); + }); + } + + @Override + public void processMessageReadEvent(MessageReadEvent messageReadEvent) { + + } + + @Override + public void processTopEvent(TopEvent topEvent) { + com.juick.Message message = topEvent.getMessage(); + try { + commandsManager.processCommand(serviceUser, String.format("! #%d", message.getMid()), URI.create(StringUtils.EMPTY)); + } catch (Exception e) { + logger.warn("XMPP error", e); + } + } + + private void incomingPresence(Presence p) { + final String username = p.getTo().getLocal(); + final boolean toJuick = username.equals(jid.getLocal()); + + if (p.getType() == null) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().asBareJid()); + reply.setTo(p.getFrom().asBareJid()); + reply.setType(Presence.Type.UNSUBSCRIBE); + router.send(ClientPresence.from(reply)); + } else if (p.getType().equals(Presence.Type.PROBE)) { + int uid_to = 0; + if (!toJuick) { + uid_to = userService.getUIDbyName(username); + } else { + User visitor = userService.getUserByJID(p.getFrom().asBareJid().toEscapedString()); + if (visitor != null) { + userService.updateLastSeen(visitor); + } + } + + if (toJuick || uid_to > 0) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().withResource(jid.getResource())); + reply.setTo(p.getFrom()); + reply.setPriority((byte)10); + if (!userService.getActiveJIDs().contains(p.getFrom().asBareJid().toEscapedString())) { + reply.setStatus("Send ON to enable notifications"); + } + router.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + router.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { + boolean canSubscribe = false; + if (toJuick) { + canSubscribe = true; + } else { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + canSubscribe = true; + } + } + if (canSubscribe) { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.SUBSCRIBED); + router.send(ClientPresence.from(reply)); + + reply.setFrom(reply.getFrom().withResource(jid.getResource())); + reply.setPriority((byte) 10); + reply.setType(null); + router.send(ClientPresence.from(reply)); + } else { + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.ERROR); + reply.setId(p.getId()); + reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); + router.send(ClientPresence.from(reply)); + } + } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { + if (!toJuick) { + int uid_to = userService.getUIDbyName(username); + if (uid_to > 0) { + pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); + } + } + + Presence reply = new Presence(); + reply.setFrom(p.getTo()); + reply.setTo(p.getFrom()); + reply.setType(Presence.Type.UNSUBSCRIBED); + router.send(ClientPresence.from(reply)); + } + } + + public ClientMessage incomingMessage(Message msg) { + ClientMessage result = null; + if (msg.getType() != null && msg.getType().equals(Message.Type.ERROR)) { + StanzaError error = msg.getError(); + if (error != null && error.getCondition().equals(Condition.RESOURCE_CONSTRAINT)) { + // offline query is full, deactivating this jid + if (userService.setActiveStatusForJID(msg.getFrom().toEscapedString(), UserService.ActiveStatus.Inactive)) { + logger.info("{} is inactive now", msg.getFrom()); + return null; + } + } + return null; + } + Jid to = msg.getTo(); + if (to.getDomain().equals(router.getDomain().toEscapedString()) || to.equals(this.jid)) { + User user_from = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); + if ((user_from == null || user_from.isAnonymous()) && !msg.getFrom().equals(jid)) { + String signuphash = userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); + return makeReply(msg.getFrom(), "Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); + } + URI attachment = URI.create(StringUtils.EMPTY); + OobX oobX = msg.getExtension(OobX.class); + if (oobX != null) { + attachment = oobX.getUri(); + } + try { + return incomingMessageJuick(user_from, msg.getFrom(), msg.getTo().getLocal(), StringUtils.defaultString(msg.getBody()).trim(), attachment); + } catch (Exception e1) { + logger.warn("message exception", e1); + } + } else if (to.getDomain().endsWith(jid.getDomain()) && (to.getDomain().equals(jid.getDomain()) + || to.getDomain().endsWith("." + jid.getDomain()))) { + if (logger.isInfoEnabled()) { + try { + logger.info("unhandled message: {}", stanzaToString(msg)); + } catch (JAXBException | XMLStreamException ex) { + logger.error("JAXB exception", ex); + } + } + } else { + return ClientMessage.from(msg); + } + return result; + } + private ClientMessage incomingMessageJuick(User user_from, Jid from, String to, String command, @Nonnull URI attachment) { + if (StringUtils.isBlank(command) && attachment.toString().isEmpty()) { + return null; + } + + messagesService.getUnread(user_from).forEach(mid -> messagesService.setRead(user_from, mid)); + + int commandlen = command.length(); + + // COMPATIBILITY + if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { + command = command.substring(3); + } + + if (!jid.getLocal().equals(to)) { + // PM + if (!StringUtils.isEmpty(command)) { + commandsManager.commandPM(user_from, null, to, command); + return null; + } + } + + try { + CommandResult result = commandsManager.processCommand(user_from, command, attachment); + if (StringUtils.isNotBlank(result.getText())) { + return makeReply(from, result.getText()); + } + } catch (Exception e) { + logger.warn("xmpp command exception", e); + return makeReply(from, "Error processing command"); + } + return null; + } + + private void broadcastPresence(Presence.Type type) { + Presence presence = new Presence(); + presence.setFrom(jid); + if (type != null) { + presence.setType(type); + } + userService.getActiveJIDs().forEach(j -> { + try { + presence.setTo(Jid.of(j)); + router.send(ClientPresence.from(presence)); + } catch (IllegalArgumentException ex) { + logger.warn("Invalid jid: {}", j, ex); + } + }); + } + + @PreDestroy + public void close() throws Exception { + broadcastPresence(Presence.Type.UNAVAILABLE); + if (router != null) { + router.close(); + } + } + + public ExternalComponent getRouter() { + return router; + } +} diff --git a/src/main/java/com/juick/server/configuration/XMPPConfig.java b/src/main/java/com/juick/server/configuration/XMPPConfig.java index ffacf4ef..f9b6f092 100644 --- a/src/main/java/com/juick/server/configuration/XMPPConfig.java +++ b/src/main/java/com/juick/server/configuration/XMPPConfig.java @@ -1,21 +1,13 @@ package com.juick.server.configuration; -import com.juick.server.XMPPConnection; +import com.juick.server.XMPPManager; import com.juick.server.xmpp.JidConverter; -import com.juick.server.xmpp.iq.MessageQuery; -import com.juick.server.xmpp.s2s.BasicXmppSession; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import rocks.xmpp.core.session.Extension; -import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.session.debug.LogbackDebugger; - -import java.time.Duration; @Configuration @ConditionalOnProperty("xmppbot_jid") @@ -23,22 +15,13 @@ public class XMPPConfig { @Value("${hostname:localhost}") private String hostname; @Bean - public BasicXmppSession session() { - XmppSessionConfiguration configuration = XmppSessionConfiguration.builder() - .extensions(Extension.of(com.juick.Message.class), Extension.of(MessageQuery.class)) - .debugger(LogbackDebugger.class) - .defaultResponseTimeout(Duration.ofMillis(120000)) - .build(); - return BasicXmppSession.create(hostname, configuration); - } - @Bean public static ConversionService conversionService() { DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); cs.addConverter(new JidConverter()); return cs; } @Bean - public XMPPConnection xmppConnection() { - return new XMPPConnection(); + public XMPPManager xmppConnection() { + return new XMPPManager(); } } diff --git a/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java b/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java deleted file mode 100644 index ae28f827..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/BasicXmppSession.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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/StanzaListener.java b/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java deleted file mode 100644 index 6932298f..00000000 --- a/src/main/java/com/juick/server/xmpp/s2s/StanzaListener.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.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/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 620e210c..fa2e2ce9 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -87,9 +87,6 @@ import rocks.xmpp.addr.Jid; import rocks.xmpp.core.session.Extension; import rocks.xmpp.core.session.XmppSession; import rocks.xmpp.core.session.XmppSessionConfiguration; -import rocks.xmpp.core.stanza.model.StanzaError; -import rocks.xmpp.core.stanza.model.client.ClientMessage; -import rocks.xmpp.core.stanza.model.errors.Condition; import javax.inject.Inject; import javax.servlet.http.Cookie; @@ -101,7 +98,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; -import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -113,8 +109,6 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.*; import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; @@ -161,8 +155,6 @@ public class ServerTests { @Inject private CommandsManager commandsManager; @Inject - private XMPPConnection router; - @Inject private SubscriptionService subscriptionService; @Inject private PrivacyQueriesService privacyQueriesService; -- cgit v1.2.3 From 809ef60e18bb8ab7c95db93b7777f3c0ffb30872 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 20 Dec 2018 09:41:32 +0300 Subject: HTTPSignatureAuthenticationFilter --- .../java/com/juick/server/KeystoreManager.java | 9 +-- .../java/com/juick/server/SignatureManager.java | 60 +++++++++++------ .../com/juick/server/api/activity/Profile.java | 26 ++----- .../server/configuration/BaseWebConfiguration.java | 10 +++ .../juick/server/configuration/SecurityConfig.java | 5 ++ .../HTTPSignatureAuthenticationFilter.java | 68 +++++++++++++++++++ .../configuration/TestActivityConfiguration.java | 19 ++++++ .../java/com/juick/server/tests/ServerTests.java | 75 +++++++++++++++++++-- src/test/resources/mocks/activity/testfollow.json | 15 +++++ src/test/resources/mocks/activity/testuser.json | 27 ++++++++ src/test/resources/test.p12 | Bin 0 -> 2386 bytes 11 files changed, 260 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java create mode 100644 src/test/java/com/juick/server/configuration/TestActivityConfiguration.java create mode 100644 src/test/resources/mocks/activity/testfollow.json create mode 100644 src/test/resources/mocks/activity/testuser.json create mode 100644 src/test/resources/test.p12 (limited to 'src/test/java') diff --git a/src/main/java/com/juick/server/KeystoreManager.java b/src/main/java/com/juick/server/KeystoreManager.java index 67a24f11..3ae7b866 100644 --- a/src/main/java/com/juick/server/KeystoreManager.java +++ b/src/main/java/com/juick/server/KeystoreManager.java @@ -19,20 +19,17 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.stream.Collectors; -@Component public class KeystoreManager { private static final Logger logger = LoggerFactory.getLogger("com.juick.server"); - @Value("${keystore:juick.p12}") - private String keystore; - @Value("${keystore_password:secret}") + private String keystorePassword; private KeyStore ks; private KeyManagerFactory kmf; - @PostConstruct - public void init() { + public KeystoreManager(String keystore, String keystorePassword) { + this.keystorePassword = keystorePassword; try (InputStream ksIs = new FileInputStream(keystore)) { ks = KeyStore.getInstance("PKCS12"); ks.load(ksIs, keystorePassword.toCharArray()); diff --git a/src/main/java/com/juick/server/SignatureManager.java b/src/main/java/com/juick/server/SignatureManager.java index 9ecdaad5..23f5c37a 100644 --- a/src/main/java/com/juick/server/SignatureManager.java +++ b/src/main/java/com/juick/server/SignatureManager.java @@ -2,6 +2,7 @@ package com.juick.server; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.User; +import com.juick.model.AnonymousUser; import com.juick.server.api.activity.model.Context; import com.juick.server.api.activity.model.objects.Person; import com.juick.server.api.webfinger.model.Account; @@ -11,7 +12,6 @@ import com.juick.util.DateFormattersHolder; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -53,28 +53,43 @@ public class SignatureManager { URI inbox = uriComponentsBuilder.build().toUri(); Instant now = Instant.now(); String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); - Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, - "(request-target)", "host", "date"); - Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature); - Map headers = new HashMap<>(); - headers.put("host", inbox.getHost()); - headers.put("date", requestDate); - Signature signature = signer.sign("POST", inbox.getPath(), headers); + String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost(); + String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate); + HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE); requestHeaders.add("Date", requestDate); - requestHeaders.add("Signature", signature.toString().substring(10)); + requestHeaders.add("Host", host); + requestHeaders.add("Signature", signatureString); HttpEntity request = new HttpEntity<>(Context.build(data), requestHeaders); - //boolean valid = verifySignature(Signature.fromString(requestHeaders.getFirst("Signature")), - // keystoreManager.getPublicKey(), "POST", inbox.getPath(), headers); logger.info("Sending context: {}", jsonMapper.writeValueAsString(data)); logger.info("Request date: {}", requestDate); ResponseEntity response = apClient.postForEntity(inbox, request, Void.class); logger.info("accepted follower: {}", response.getStatusCodeValue()); + } + + public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException { + return addSignature(from, host, method, path, dateString, keystoreManager); + } + public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException { + Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, + "(request-target)", "host", "date"); + Map headers = new HashMap<>(); + headers.put("host", host); + headers.put("date", dateString); + Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature); + Signature signature = signer.sign(method, path, headers); + // remove "Signature: " from result + return signature.toString().substring(10); } + public User verifySignature(String method, String path, Map headers) throws IOException { - Signature signature = Signature.fromString(headers.get("signature")); + String signatureString = headers.get("signature"); + if (StringUtils.isEmpty(signatureString)) { + return AnonymousUser.INSTANCE; + } + Signature signature = Signature.fromString(signatureString); Optional context = getContext(URI.create(signature.getKeyId())); if (context.isPresent() && context.get() instanceof Person) { Person person = (Person) context.get(); @@ -84,12 +99,16 @@ public class SignatureManager { try { boolean result = verifier.verify(method, path, headers); logger.info("signature is valid: {}", result); - User user = new User(); - user.setUri(URI.create(person.getId())); - if (key.equals(keystoreManager.getPublicKey())) { - return userService.getUserByName(person.getName()); + if (result) { + User user = new User(); + user.setUri(URI.create(person.getId())); + if (key.equals(keystoreManager.getPublicKey())) { + return userService.getUserByName(person.getName()); + } + return user; + } else { + return AnonymousUser.INSTANCE; } - return user; } catch (NoSuchAlgorithmException | SignatureException | IOException e) { throw new IOException("Invalid signature"); } @@ -110,9 +129,12 @@ public class SignatureManager { return Optional.empty(); } public Optional discoverPerson(String acct) { - Jid acctId = Jid.of(acct); + String[] accountParts = acct.split(":", 2); + String account = accountParts[0]; + int port = accountParts.length > 1 ? Integer.valueOf(accountParts[1]) : 80; + Jid acctId = Jid.of(account); URI resourceUri = UriComponentsBuilder.fromUriString( - String.format("https://%s/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), acct)).build().toUri(); + String.format("http://%s:%d/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), port, account)).build().toUri(); Account acctData = apClient.getForEntity(resourceUri, Account.class).getBody(); if (acctData != null) { for (Link l : acctData.getLinks()) { diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java index 2614cded..404f0f84 100644 --- a/src/main/java/com/juick/server/api/activity/Profile.java +++ b/src/main/java/com/juick/server/api/activity/Profile.java @@ -44,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -252,30 +253,11 @@ public class Profile { } @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) - public ResponseEntity processInbox(@RequestBody Activity activity, - @RequestHeader(name = "Host") String host, - @RequestHeader(name = "Date") String date, - @RequestHeader(name = "Digest", required = false) String digest, - @RequestHeader(name = "Content-Type") String contentType, - @RequestHeader(name = "User-Agent", required = false) String userAgent, - @RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding, - @RequestHeader(name = "Signature", required = false) String signature) throws Exception { - UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); - Map headers = new HashMap<>(); - headers.put("host", host.split(":", 2)[0]); - headers.put("date", date); - headers.put("digest", digest); - headers.put("content-type", contentType); - headers.put("user-agent", userAgent); - headers.put("accept-encoding", acceptEncoding); - headers.put("signature", signature); - User signedUser = signatureManager.verifySignature( "POST", - componentsBuilder.getPath(), headers); - if ((StringUtils.isNotEmpty(signedUser.getUri().toString()) && signedUser.getUri().equals(URI.create(activity.getActor()))) || !signedUser.isAnonymous()) { + public ResponseEntity processInbox(@RequestBody Activity activity) throws Exception { + User visitor = UserUtils.getCurrentUser(); + if ((StringUtils.isNotEmpty(visitor.getUri().toString()) && visitor.getUri().equals(URI.create(activity.getActor()))) || !visitor.isAnonymous()) { if (activity instanceof Follow) { Follow followRequest = (Follow) activity; - String actor = followRequest.getActor(); - Person follower = (Person) signatureManager.getContext(URI.create(actor)).orElseThrow(HttpBadRequestException::new); applicationEventPublisher.publishEvent( new FollowEvent(this, followRequest)); return new ResponseEntity<>(HttpStatus.ACCEPTED); diff --git a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java index 6a2a8142..16693995 100644 --- a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java +++ b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java @@ -17,6 +17,8 @@ package com.juick.server.configuration; +import com.juick.server.KeystoreManager; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; @@ -36,6 +38,10 @@ import java.util.concurrent.Executors; @Configuration public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigurer { + @Value("${keystore:juick.p12}") + private String keystore; + @Value("${keystore_password:secret}") + private String keystorePassword; @Override public void configurePathMatch(PathMatchConfigurer configurer) { @@ -61,4 +67,8 @@ public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigu public ExecutorService executorService() { return Executors.newCachedThreadPool(); } + @Bean + public KeystoreManager keystoreManager() { + return new KeystoreManager(keystore, keystorePassword); + } } diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java index 7145e9d5..d2d3ab13 100644 --- a/src/main/java/com/juick/server/configuration/SecurityConfig.java +++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -17,7 +17,9 @@ package com.juick.server.configuration; +import com.juick.server.SignatureManager; import com.juick.service.UserService; +import com.juick.service.security.HTTPSignatureAuthenticationFilter; import com.juick.service.security.HashParamAuthenticationFilter; import com.juick.service.security.JuickUserDetailsService; import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; @@ -93,6 +95,8 @@ public class SecurityConfig { private String webDomain; @Resource private UserService userService; + @Resource + private SignatureManager signatureManager; ApiConfig() { super(true); } @@ -109,6 +113,7 @@ public class SecurityConfig { protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/api/**") .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) + .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java new file mode 100644 index 00000000..8332fc8c --- /dev/null +++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java @@ -0,0 +1,68 @@ +package com.juick.service.security; + +import com.juick.User; +import com.juick.server.SignatureManager; +import com.juick.service.UserService; +import com.juick.service.security.entities.JuickUser; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Nonnull; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter { + + private final SignatureManager signatureManager; + private final UserService userService; + + + public HTTPSignatureAuthenticationFilter( + final SignatureManager signatureManager, + final UserService userService) { + this.signatureManager = signatureManager; + this.userService = userService; + } + @Override + protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain filterChain) throws IOException, ServletException { + if (authenticationIsRequired()) { + Map headers = Collections.list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(String::toLowerCase, request::getHeader)); + User user = signatureManager.verifySignature(request.getMethod(), request.getRequestURI(), headers); + if (!user.isAnonymous()) { + String userUri = user.getUri().toString(); + if (userUri.length() == 0) { + User userWithPassword = userService.getUserByName(user.getName()); + userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid())); + Authentication authentication = new UsernamePasswordAuthenticationToken(userWithPassword.getName(), userWithPassword.getCredentials()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + Authentication authentication = new AnonymousAuthenticationToken(userUri, user, Collections.singletonList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + + filterChain.doFilter(request, response); + } + + private boolean authenticationIsRequired() { + Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); + + return existingAuth == null || + !existingAuth.isAuthenticated() || + existingAuth instanceof AnonymousAuthenticationToken; + } +} diff --git a/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java b/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java new file mode 100644 index 00000000..5daf4900 --- /dev/null +++ b/src/test/java/com/juick/server/configuration/TestActivityConfiguration.java @@ -0,0 +1,19 @@ +package com.juick.server.configuration; + +import com.juick.server.KeystoreManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@Configuration +public class TestActivityConfiguration { + @Value("classpath:test.p12") + Resource keystoreFile; + @Bean + public KeystoreManager testKeystoreManager() throws IOException { + return new KeystoreManager(keystoreFile.getFile().getAbsolutePath(), "secret"); + } +} diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index fa2e2ce9..fedbaba0 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -65,10 +65,13 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.*; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -76,6 +79,7 @@ import org.springframework.util.DigestUtils; import org.springframework.util.FileSystemUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.w3c.dom.Document; @@ -118,6 +122,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.client.ExpectedCount.times; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -130,9 +137,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @TestPropertySource(properties = { "broken_ssl_hosts=localhost,serverstorageisfull.tld", "ios_app_id=12345678.com.juick.ExampleApp", - "xmppbot_jid=juick@localhost/Juick", - "hostname=localhost", - "componentname=localhost", "spring.jackson.default-property-inclusion=non_default" }) @AutoConfigureMockMvc @@ -174,10 +178,6 @@ public class ServerTests { private ServerManager serverManager; @Inject private KeystoreManager keystoreManager; - @Value("${hostname:localhost}") - private Jid jid; - @Value("${xmppbot_jid:juick@localhost}") - private Jid botJid; @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String tmpDir; @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") @@ -192,6 +192,18 @@ public class ServerTests { private ActivityPubManager activityPubManager; @Inject private WebApp webApp; + @Inject + private RestTemplate apClient; + + @Value("classpath:mocks/activity/testuser.json") + private Resource testuserResponse; + @Value("classpath:mocks/activity/testfollow.json") + private Resource testfollowRequest; + + @Inject + private KeystoreManager testKeystoreManager; + + private MockRestServiceServer restServiceServer; private static User ugnich, freefd, juick; static String ugnichName, ugnichPassword, freefdName, freefdPassword, juickName, juickPassword; @@ -1699,6 +1711,55 @@ public class ServerTests { signatureManager.post(from, to, follow); } @Test + public void serviceSignatureAuth() throws Exception { + String meUri = "/api/me"; + String testHost = "localhost:8080"; + Person ugnichPerson = (Person) signatureManager.discoverPerson("ugnich@localhost:8080").get(); + Instant now = Instant.now(); + String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now); + String signatureString = signatureManager.addSignature(ugnichPerson, testHost, "GET", meUri, requestDate); + MvcResult me = mockMvc.perform(get("/api/me") + .header("Host", testHost) + .header("Date", requestDate) + .header("Signature", signatureString)) + .andExpect(status().isOk()) + .andReturn(); + User meUser = jsonMapper.readValue(me.getResponse().getContentAsString(), User.class); + assertThat(meUser, is(ugnich)); + String testuserResponseString = IOUtils.toString(testuserResponse.getInputStream(), StandardCharsets.UTF_8); + ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory(); + restServiceServer = MockRestServiceServer.bindTo(apClient).build(); + URI testuserUri = URI.create("https://example.com/u/testuser"); + URI testuserkeyUri = URI.create("https://example.com/u/testuser#main-key"); + restServiceServer.expect(times(3), requestTo(testuserUri)) + .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON_UTF8)); + restServiceServer.expect(times(3), requestTo(testuserkeyUri)) + .andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON_UTF8)); + Person testuser = (Person)signatureManager.getContext(testuserUri).get(); + Assert.assertThat(testuser.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem())); + Instant now2 = Instant.now(); + String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2); + String inboxUri = "/api/inbox"; + String testSignatureString = + signatureManager.addSignature(testuser, testHost, "POST", + inboxUri, testRequestDate, testKeystoreManager); + mockMvc.perform(post(inboxUri) + .header("Host", testHost) + .header("Date", testRequestDate) + .header("Signature", testSignatureString) + .contentType(Context.LD_JSON_MEDIA_TYPE) + .content(IOUtils.toByteArray(testfollowRequest.getInputStream()))) + .andExpect(status().isAccepted()); + mockMvc.perform(post(inboxUri) + .header("Host", "wronghost") + .header("Date", testRequestDate) + .header("Signature", testSignatureString) + .contentType(Context.LD_JSON_MEDIA_TYPE) + .content(IOUtils.toByteArray(testfollowRequest.getInputStream()))) + .andExpect(status().isUnauthorized()); + apClient.setRequestFactory(originalRequestFactory); + } + @Test public void hostmeta() throws Exception { MvcResult result = mockMvc.perform(get("/.well-known/host-meta")) .andExpect(status().isOk()).andReturn(); diff --git a/src/test/resources/mocks/activity/testfollow.json b/src/test/resources/mocks/activity/testfollow.json new file mode 100644 index 00000000..e308e52e --- /dev/null +++ b/src/test/resources/mocks/activity/testfollow.json @@ -0,0 +1,15 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://example.com/12345678", + "type": "Follow", + "actor": "https://example.com/u/testuser", + "object": "http://localhost:8080/u/ugnich" +} \ No newline at end of file diff --git a/src/test/resources/mocks/activity/testuser.json b/src/test/resources/mocks/activity/testuser.json new file mode 100644 index 00000000..95fc2aa9 --- /dev/null +++ b/src/test/resources/mocks/activity/testuser.json @@ -0,0 +1,27 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://example.com/u/testuser", + "type": "Person", + "following": "https://example.com/u/testuser/following", + "followers": "https://example.com/u/testuser/followers", + "inbox": "https://example.com/u/testuser/inbox", + "outbox": "https://example.com/u/testuser/outbox", + "preferredUsername": "testuser", + "url": "https://example.com/@testuser", + "publicKey": { + "id": "https://example.com/u/testuser#main-key", + "owner": "https://example.com/u/testuser", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiHKRdKFFeT4P/MVlNbxC\nbbgXOkEdeQzvJB/wAJgSYbUwm9SzNFzttePQXk3/MWoK2awWUInZTduVHsWt8zU7\nO3d9PAW6YH6L1oDkjgMLAb9aUWV2ClQWMwsn88WKK9Rb1WOmd8BrXjPfmeFK2ypQ\n9eg8aKpH36WAXiiaTDfBupBZ0Ki2+E87BrWxpbUeDC1dkV+zbl8BMm7X0rp+reoC\nYUWMcjQMzhMmQOXUd4zwJIDPZDMdF4beq/y6WPSUTVgjs4kPDS1HT60ATnsUqyPE\n6tuGxG4j0msb4TTre87PKxMU5YPOxSiqNL0O/3u9/2shVPpjDa/uy9W+VaeBHbFm\nSQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "endpoints": { + "sharedInbox": "https://example.com/inbox" + } +} \ No newline at end of file diff --git a/src/test/resources/test.p12 b/src/test/resources/test.p12 new file mode 100644 index 00000000..7f7457eb Binary files /dev/null and b/src/test/resources/test.p12 differ -- cgit v1.2.3 From 41b6764be12c2b21f0de55b8a80091e279577ab5 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 21 Jan 2019 14:49:07 +0300 Subject: ActivityPub: convert HTML to Markdown --- build.gradle | 1 + src/main/java/com/juick/server/api/activity/Profile.java | 9 +++++++-- .../com/juick/server/configuration/BaseWebConfiguration.java | 8 ++++++++ src/main/java/com/juick/service/MessagesServiceImpl.java | 2 +- src/test/java/com/juick/MessageTest.java | 10 ++++++++++ src/test/java/com/juick/server/tests/ServerTests.java | 12 ++++++------ 6 files changed, 33 insertions(+), 9 deletions(-) (limited to 'src/test/java') diff --git a/build.gradle b/build.gradle index 7deec746..fcf52dfc 100644 --- a/build.gradle +++ b/build.gradle @@ -180,6 +180,7 @@ dependencies { compile 'org.tomitribe:tomitribe-http-signatures:1.1' compile 'com.squareup.okhttp3:okhttp:3.12.1' compile 'com.google.api-client:google-api-client:1.28.0' + compile "com.kotcrab.remark:remark:1.0.0" testCompile("org.springframework.boot:spring-boot-starter-test") testCompile('net.sourceforge.htmlunit:htmlunit:2.33') diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java index 404f0f84..3e0179c8 100644 --- a/src/main/java/com/juick/server/api/activity/Profile.java +++ b/src/main/java/com/juick/server/api/activity/Profile.java @@ -29,6 +29,7 @@ import com.juick.server.www.WebApp; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.service.activities.*; +import com.overzealous.remark.Remark; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,6 +83,8 @@ public class Profile { private ObjectMapper jsonMapper; @Inject private WebApp webApp; + @Inject + private Remark remarkConverter; @GetMapping(value = "/u/{userName}", produces = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) public Person getUser(@PathVariable String userName) { @@ -301,7 +304,8 @@ public class Profile { Map attachmentObj = (Map) ((List) note.get("attachment")).get(0); attachment = (String) attachmentObj.get("url"); } - CommandResult result = commandsManager.processCommand(user, String.format("#%s %s", postId, note.get("content")), URI.create(attachment)); + String markdown = remarkConverter.convertFragment((String)note.get("content")); + CommandResult result = commandsManager.processCommand(user, String.format("#%s %s", postId, markdown), URI.create(attachment)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { messagesService.updateReplyUri(result.getNewMessage().get(), noteId); @@ -319,7 +323,8 @@ public class Profile { Map attachmentObj = (Map) ((List) note.get("attachment")).get(0); attachment = (String) attachmentObj.get("url"); } - CommandResult result = commandsManager.processCommand(user, String.format("#%d/%d %s", reply.getMid(), reply.getRid(), note.get("content")), URI.create(attachment)); + String markdown = remarkConverter.convertFragment((String)note.get("content")); + CommandResult result = commandsManager.processCommand(user, String.format("#%d/%d %s", reply.getMid(), reply.getRid(), markdown), URI.create(attachment)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { messagesService.updateReplyUri(result.getNewMessage().get(), noteId); diff --git a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java index 16693995..c8b88cd1 100644 --- a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java +++ b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java @@ -18,6 +18,8 @@ package com.juick.server.configuration; import com.juick.server.KeystoreManager; +import com.overzealous.remark.Options; +import com.overzealous.remark.Remark; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -71,4 +73,10 @@ public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigu public KeystoreManager keystoreManager() { return new KeystoreManager(keystore, keystorePassword); } + @Bean + public Remark remarkConverter() { + Options options = new Options(); + options.inlineLinks = true; + return new Remark(options); + } } diff --git a/src/main/java/com/juick/service/MessagesServiceImpl.java b/src/main/java/com/juick/service/MessagesServiceImpl.java index 245ae783..c3d319d5 100644 --- a/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -1139,7 +1139,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ @Override public boolean updateReplyUri(Message reply, URI replyUri) { - return jdbcTemplate.update("UPDATE replies SET reply_uri=?, html=1 WHERE message_id=? AND reply_id=?", + return jdbcTemplate.update("UPDATE replies SET reply_uri=?, html=0 WHERE message_id=? AND reply_id=?", replyUri.toASCIIString(), reply.getMid(), reply.getRid()) > 0; } diff --git a/src/test/java/com/juick/MessageTest.java b/src/test/java/com/juick/MessageTest.java index 4c37a678..2d12a3df 100644 --- a/src/test/java/com/juick/MessageTest.java +++ b/src/test/java/com/juick/MessageTest.java @@ -20,6 +20,8 @@ package com.juick; import com.juick.model.Entity; import com.juick.test.util.MockUtils; import com.juick.util.MessageUtils; +import com.overzealous.remark.Options; +import com.overzealous.remark.Remark; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.junit.Test; @@ -196,4 +198,12 @@ public class MessageTest { assertThat(yo.getStart(), is(0)); assertThat(yo.getEnd(), is(7)); } + @Test + public void ActivityStreamsHTMLtoMarkdownTest() { + String text = "

@stanislavv я в @rf выкладывал =)

"; + Options options = new Options(); + options.inlineLinks = true; + Remark remark = new Remark(options); + String parsed = remark.convertFragment(text); + } } diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index fedbaba0..f1087d8c 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -442,7 +442,7 @@ public class ServerTests { @Test public void homeTestWithMessages() throws Exception { String msgText = "Привет, я - Угнич"; - CommandResult result = commandsManager.processCommand(ugnich, msgText, URI.create("http://static.juick.com/settings/facebook.png")); + CommandResult result = commandsManager.processCommand(ugnich, msgText, URI.create("https://static.juick.com/settings/facebook.png")); int mid = result.getNewMessage().get().getMid(); Message msg = messagesService.getMessage(mid).get(); tagService.createTag("тест"); @@ -697,15 +697,15 @@ public class ServerTests { int uid = userService.createUser("me", "secret"); User user = userService.getUserByUID(uid).orElse(AnonymousUser.INSTANCE); Tag yo = tagService.getTag("yo", true); - Message msg = commandsManager.processCommand(user, "*yo yoyo", URI.create("http://static.juick.com/settings/facebook.png")).getNewMessage().get(); + Message msg = commandsManager.processCommand(user, "*yo yoyo", URI.create("https://static.juick.com/settings/facebook.png")).getNewMessage().get(); assertThat(msg.getAttachmentType(), is("png")); - Message msgreply = commandsManager.processCommand(user, "#" + msg.getMid() + " yyy", HttpUtils.downloadImage(URI.create("http://static.juick.com/settings/xmpp.png").toURL(), tmpDir)).getNewMessage().get(); + Message msgreply = commandsManager.processCommand(user, "#" + msg.getMid() + " yyy", HttpUtils.downloadImage(URI.create("https://static.juick.com/settings/xmpp.png").toURL(), tmpDir)).getNewMessage().get(); assertThat(msgreply.getAttachmentType(), equalTo("png")); assertEquals("text should match", "yoyo", messagesService.getMessage(msg.getMid()).get().getText()); assertEquals("tag should match", "yo", tagService.getMessageTags(msg.getMid()).get(0).getTag().getName()); - CommandResult yoyoMsg = commandsManager.processCommand(user, "*yo", URI.create("http://static.juick.com/settings/facebook.png")); + CommandResult yoyoMsg = commandsManager.processCommand(user, "*yo", URI.create("https://static.juick.com/settings/facebook.png")); assertTrue(yoyoMsg.getNewMessage().isPresent()); assertThat(yoyoMsg.getNewMessage().get().getTags().get(0), is(yo)); Message msg2 = yoyoMsg.getNewMessage().get(); @@ -749,10 +749,10 @@ public class ServerTests { String expectedThirdReply = "Reply posted.\n#" + mid + "/3 " + "https://juick.com/m/" + mid + "#3"; assertEquals("should be second reply", expectedSecondReply, - commandsManager.processCommand(user, "#" + mid + " yoyo", URI.create("http://static.juick.com/settings/facebook.png")).getText()); + commandsManager.processCommand(user, "#" + mid + " yoyo", URI.create("https://static.juick.com/settings/facebook.png")).getText()); assertEquals("should be third reply", expectedThirdReply, commandsManager.processCommand(user, " \t\n #" + mid + "/2 ", - URI.create("http://static.juick.com/settings/facebook.png")).getText()); + URI.create("https://static.juick.com/settings/facebook.png")).getText()); Message reply = messagesService.getReplies(user, mid).stream().filter(m -> m.getRid() == 3).findFirst() .orElse(new Message()); Timestamp lastreply = jdbcTemplate.queryForObject("SELECT lastmessage FROM users WHERE id=?", Timestamp.class, user.getUid()); -- cgit v1.2.3 From 0b418caf401c53eb73721a324cd13041c185d0fb Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 22 Jan 2019 20:52:52 +0300 Subject: Fix quote regex --- src/main/java/com/juick/util/MessageUtils.java | 10 ++++------ src/test/java/com/juick/MessageTest.java | 9 +++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'src/test/java') diff --git a/src/main/java/com/juick/util/MessageUtils.java b/src/main/java/com/juick/util/MessageUtils.java index 5cc8bb97..72c9bd04 100644 --- a/src/main/java/com/juick/util/MessageUtils.java +++ b/src/main/java/com/juick/util/MessageUtils.java @@ -87,7 +87,7 @@ public class MessageUtils { private final static String underlineRegex = "((?<=\\s)|(?<=\\A))_([^\\_\\n<>]+)_((?=\\s)|(?=\\Z)|(?=\\p{Punct}))"; - private final static String citateRegex = "(?:(?<=\\n)|(?<=\\A))\\> *(.*)?(\\n|(?=\\Z))"; + private final static String citateRegex = "(?:(?<=\\n)|(?<=\\A))> *(.*)?(\\n|(?=\\Z))"; public static List getEntities(Message msg) { @@ -144,14 +144,12 @@ public class MessageUtils { } public static String formatMessage(String msg) { - // > citate - msg = msg.replaceAll(citateRegex, "$1"); - msg = msg.replaceAll("", "\n"); - - msg = msg.replaceAll("&", "&"); msg = msg.replaceAll("<", "<"); msg = msg.replaceAll(">", ">"); + // > citate + msg = msg.replaceAll(citateRegex, "$1"); + msg = msg.replaceAll("", "\n"); // -- // — msg = msg.replaceAll("((?<=\\s)|(?<=\\A))\\-\\-?((?=\\s)|(?=\\Z))", "$1—$2"); diff --git a/src/test/java/com/juick/MessageTest.java b/src/test/java/com/juick/MessageTest.java index 2d12a3df..0b48efa1 100644 --- a/src/test/java/com/juick/MessageTest.java +++ b/src/test/java/com/juick/MessageTest.java @@ -206,4 +206,13 @@ public class MessageTest { Remark remark = new Remark(options); String parsed = remark.convertFragment(text); } + @Test + public void messageFormatTest() { + String msg = "> quote\nmessage"; + assertThat(MessageUtils.formatMessage(msg), is("quotemessage")); + String brokenComment = "