├── xmpp-react-client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── features │ │ ├── contacts │ │ │ ├── contactActions.js │ │ │ ├── ContactsBoxContainer.js │ │ │ ├── AddContactBoxContainer.js │ │ │ ├── ContactContainer.js │ │ │ ├── contactsSlice.js │ │ │ ├── ContactsBox.js │ │ │ ├── Contact.js │ │ │ └── AddContactBox.js │ │ ├── messages │ │ │ ├── ChatBoxContainer.js │ │ │ ├── TypingAreaContainer.js │ │ │ ├── typingAreaActions.js │ │ │ ├── messagesSlice.js │ │ │ ├── Message.js │ │ │ ├── ChatBox.js │ │ │ └── TypingArea.js │ │ ├── home │ │ │ ├── HomeContainer.js │ │ │ └── Home.js │ │ ├── user │ │ │ ├── LoginContainer.js │ │ │ ├── userSlice.js │ │ │ └── Login.js │ │ ├── current │ │ │ └── currentSlice.js │ │ └── alert │ │ │ └── alertSlice.js │ ├── app │ │ ├── browserhistory.js │ │ ├── App.js │ │ ├── store.js │ │ └── Router.js │ ├── common │ │ └── middleware │ │ │ ├── websocketActions.js │ │ │ └── websocketMiddleware.js │ ├── setupTests.js │ ├── index.js │ ├── index.css │ └── serviceWorker.js ├── .vscode │ └── settings.json ├── .gitignore └── package.json ├── im-system-diagram.jpg ├── spring-xmpp-websocket-server ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ ├── maven-wrapper.properties │ │ └── MavenWrapperDownloader.java ├── src │ ├── main │ │ ├── resources │ │ │ ├── application-dev.properties │ │ │ ├── application.properties │ │ │ └── db │ │ │ │ └── changelog │ │ │ │ └── db.changelog-master.yaml │ │ └── java │ │ │ └── com │ │ │ └── sergiomartinrubio │ │ │ └── springxmppwebsocketsecurity │ │ │ ├── model │ │ │ ├── MessageType.java │ │ │ ├── WebsocketMessage.java │ │ │ └── Account.java │ │ │ ├── repository │ │ │ └── AccountRepository.java │ │ │ ├── Application.java │ │ │ ├── exception │ │ │ └── XMPPGenericException.java │ │ │ ├── utils │ │ │ └── BCryptUtils.java │ │ │ ├── websocket │ │ │ ├── utils │ │ │ │ ├── MessageEncoder.java │ │ │ │ ├── MessageDecoder.java │ │ │ │ └── WebSocketTextMessageHelper.java │ │ │ └── ChatWebSocket.java │ │ │ ├── config │ │ │ ├── SpringContext.java │ │ │ └── WebSocketConfig.java │ │ │ ├── service │ │ │ └── AccountService.java │ │ │ ├── xmpp │ │ │ ├── XMPPProperties.java │ │ │ ├── XMPPMessageTransmitter.java │ │ │ └── XMPPClient.java │ │ │ └── facade │ │ │ └── XMPPFacade.java │ └── test │ │ └── java │ │ └── com │ │ └── sergiomartinrubio │ │ └── springxmppwebsocketsecurity │ │ └── service │ │ └── XMPPFacadeTest.java ├── Dockerfile ├── .gitignore ├── docker-compose.yml ├── pom.xml ├── mvnw.cmd └── mvnw ├── .gitignore └── README.md /xmpp-react-client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /im-system-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartinrub/spring-xmpp-websocket-reactjs/HEAD/im-system-diagram.jpg -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/contactActions.js: -------------------------------------------------------------------------------- 1 | export const addContact = msg => ({ type: 'ADD_CONTACT', msg }); 2 | -------------------------------------------------------------------------------- /xmpp-react-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartinrub/spring-xmpp-websocket-reactjs/HEAD/xmpp-react-client/public/favicon.ico -------------------------------------------------------------------------------- /xmpp-react-client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartinrub/spring-xmpp-websocket-reactjs/HEAD/xmpp-react-client/public/logo192.png -------------------------------------------------------------------------------- /xmpp-react-client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartinrub/spring-xmpp-websocket-reactjs/HEAD/xmpp-react-client/public/logo512.png -------------------------------------------------------------------------------- /xmpp-react-client/src/app/browserhistory.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | export const history = createBrowserHistory(); 3 | -------------------------------------------------------------------------------- /xmpp-react-client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": false, 4 | "editor.formatOnPaste": false 5 | } 6 | -------------------------------------------------------------------------------- /xmpp-react-client/src/app/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MyRouter from "./Router"; 3 | 4 | function App() { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartinrub/spring-xmpp-websocket-reactjs/HEAD/spring-xmpp-websocket-server/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/ChatBoxContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import ChatBox from "./ChatBox"; 3 | 4 | export const ChatBoxContainer = connect()(ChatBox); 5 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://localhost:5432/chat 2 | 3 | xmpp.port=5222 4 | xmpp.host=localhost 5 | xmpp.domain=localhost 6 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/home/HomeContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Home from "./Home"; 3 | 4 | const HomeContainer = connect()(Home); 5 | 6 | export default HomeContainer; 7 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/TypingAreaContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import TypingArea from "./TypingArea"; 3 | 4 | export const TypingAreaContainer = connect()(TypingArea); 5 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/ContactsBoxContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import ContactsBox from "./ContactsBox"; 3 | 4 | export const ContactsBoxContainer = connect()(ContactsBox); 5 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/user/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Login from "./Login"; 3 | 4 | const LoginContainer = connect()(Login); 5 | 6 | export default LoginContainer; 7 | -------------------------------------------------------------------------------- /xmpp-react-client/src/common/middleware/websocketActions.js: -------------------------------------------------------------------------------- 1 | export const wsConnect = (username, password) => ({ 2 | type: "WS_CONNECT", 3 | username, 4 | password, 5 | }); 6 | export const wsDisconnect = () => ({ type: "WS_DISCONNECT" }); 7 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/AddContactBoxContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import AddContactBox from "./AddContactBox"; 3 | 4 | const AddContactBoxContainer = connect()(AddContactBox); 5 | 6 | export default AddContactBoxContainer; 7 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/typingAreaActions.js: -------------------------------------------------------------------------------- 1 | export const messageSent = (content) => ({ type: "MESSAGE_SENT", content }); 2 | export const newMessage = (msg) => ({ type: "NEW_MESSAGE", msg }); 3 | export const unsubscribe = msg => ({ type: 'UNSUBSCRIBE', msg }); -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:21-slim 2 | RUN mkdir /app 3 | RUN groupadd -r app && useradd -r -s /bin/false -g app app 4 | WORKDIR /app 5 | RUN mkdir temp 6 | ADD /target/spring-xmpp-websocket-server-0.1.0.jar app.jar 7 | EXPOSE 8080 8 | ENTRYPOINT ["java","-jar","/app/app.jar"] 9 | -------------------------------------------------------------------------------- /xmpp-react-client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/model/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.model; 2 | 3 | public enum MessageType { 4 | NEW_MESSAGE, JOIN_SUCCESS, LEAVE, ERROR, FORBIDDEN, ADD_CONTACT, GET_CONTACTS, UNSUBSCRIBE 5 | } 6 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.username=xmpp 2 | spring.datasource.password=password 3 | spring.datasource.url=jdbc:postgresql://spring-postgres:5432/chat 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | 6 | xmpp.port=5222 7 | xmpp.host=openfire 8 | xmpp.domain=localhost 9 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/ContactContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { select } from "../current/currentSlice"; 3 | import Contact from "./Contact"; 4 | 5 | const mapDispatchToProps = (dispatch) => { 6 | return { 7 | select: (name) => dispatch(select(name)), 8 | }; 9 | }; 10 | 11 | export const ContactContainer = connect(null, mapDispatchToProps)(Contact); 12 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/model/WebsocketMessage.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | 6 | @Value 7 | @Builder 8 | public class WebsocketMessage { 9 | String from; 10 | String to; 11 | String content; 12 | MessageType messageType; 13 | } 14 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.repository; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.Account; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface AccountRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /xmpp-react-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/Application.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/exception/XMPPGenericException.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.exception; 2 | 3 | public class XMPPGenericException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "Something went wrong when connecting to the XMPP server with username '%s'."; 6 | public XMPPGenericException(String username, Throwable e) { 7 | super(String.format(MESSAGE, username), e); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/model/Account.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.Id; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Entity 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class Account { 14 | @Id 15 | private String username; 16 | private String password; 17 | } 18 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/current/currentSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | name: "", 5 | }; 6 | 7 | export const currentSlice = createSlice({ 8 | name: "current", 9 | initialState, 10 | reducers: { 11 | select: (state, action) => { 12 | state.name = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { select } = currentSlice.actions; 18 | 19 | export const selectCurrent = (state) => state.current.name; 20 | 21 | export default currentSlice.reducer; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/**/node_modules 3 | 4 | # testing 5 | **/**/coverage 6 | 7 | # production 8 | **/**/dist 9 | **/**/build 10 | 11 | # misc 12 | **/**/.DS_Store 13 | **/**/.vscode 14 | 15 | **/**/logs 16 | **/**/*.log 17 | **/**/npm-debug.log* 18 | **/**/yarn-debug.log* 19 | **/**/yarn-error.log* 20 | 21 | **/**/target/ 22 | **/**/target/ 23 | */target/* 24 | **/**/.classpath 25 | **/**/.project 26 | **/**/.settings 27 | 28 | # Package Files # 29 | **/**/*.jar 30 | **/**/*.war 31 | **/**/*.ear 32 | 33 | **/**/*.iml 34 | **/**/*.idea 35 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/contactsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | names: [], 5 | }; 6 | 7 | export const contactsSlice = createSlice({ 8 | name: "contacts", 9 | initialState, 10 | reducers: { 11 | add: (state, action) => { 12 | state.names = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { add } = contactsSlice.actions; 18 | 19 | export const selectContacts = (state) => state.contacts.names; 20 | 21 | export default contactsSlice.reducer; 22 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/utils/BCryptUtils.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.utils; 2 | 3 | import org.springframework.security.crypto.bcrypt.BCrypt; 4 | 5 | public class BCryptUtils { 6 | 7 | public static String hash(String plainTextPassword){ 8 | return BCrypt.hashpw(plainTextPassword, BCrypt.gensalt()); 9 | } 10 | 11 | public static boolean isMatch(String plainTextPassword, String hashedPassword) { 12 | return BCrypt.checkpw(plainTextPassword, hashedPassword); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /xmpp-react-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/resources/db/changelog/db.changelog-master.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 1 4 | author: Sergio Martin Rubio 5 | changes: 6 | - createTable: 7 | tableName: account 8 | columns: 9 | - column: 10 | name: username 11 | type: varchar(255) 12 | constraints: 13 | primaryKey: true 14 | nullable: false 15 | - column: 16 | name: password 17 | type: varchar(255) 18 | constraints: 19 | nullable: false 20 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/alert/alertSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | enabled: false, 5 | message: "", 6 | }; 7 | 8 | export const alertSlice = createSlice({ 9 | name: "alert", 10 | initialState, 11 | reducers: { 12 | enableAlert: (state, action) => { 13 | state.enabled = true; 14 | state.message = action.payload; 15 | }, 16 | disableAlert: (state) => { 17 | state.enabled = false; 18 | state.message = ""; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { enableAlert, disableAlert } = alertSlice.actions; 24 | 25 | export const selectAlert = (state) => state.alert.message; 26 | 27 | export default alertSlice.reducer; 28 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/messagesSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | messages: [], 5 | }; 6 | 7 | export const messagesSlice = createSlice({ 8 | name: "messages", 9 | initialState, 10 | reducers: { 11 | addMessage: (state, action) => { 12 | const newMessage = { 13 | id: state.messages.length, 14 | content: action.payload.content, 15 | type: action.payload.type 16 | } 17 | state.messages.push(newMessage); 18 | }, 19 | }, 20 | }); 21 | 22 | export const { addMessage } = messagesSlice.actions; 23 | 24 | export const selectMessages = (state) => state.messages.messages; 25 | 26 | export default messagesSlice.reducer; 27 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/ContactsBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import AddPeopleBoxContainer from "./AddContactBoxContainer"; 4 | import { ContactContainer } from "./ContactContainer"; 5 | import { selectContacts } from "./contactsSlice"; 6 | 7 | const ContactsBox = () => { 8 | const contacts = useSelector(selectContacts); 9 | 10 | return ( 11 |
12 | 13 |
    14 | {contacts.map((name, index) => ( 15 | 16 | ))} 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default ContactsBox; 23 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/user/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | username: null, 5 | loggedIn: false, 6 | }; 7 | 8 | export const userSlice = createSlice({ 9 | name: "user", 10 | initialState, 11 | reducers: { 12 | login: (state, action) => { 13 | state.username = action.payload; 14 | state.loggedIn = true; 15 | }, 16 | logout: (state) => { 17 | state.username = null; 18 | state.loggedIn = false; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { login, logout } = userSlice.actions; 24 | 25 | export const selectUsername = (state) => state.user.username; 26 | 27 | export const selectLoggedIn = (state) => state.user.loggedIn; 28 | 29 | export default userSlice.reducer; 30 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/websocket/utils/MessageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils; 2 | 3 | import com.google.gson.Gson; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 5 | import jakarta.websocket.Encoder; 6 | import jakarta.websocket.EndpointConfig; 7 | 8 | public class MessageEncoder implements Encoder.Text { 9 | @Override 10 | public String encode(WebsocketMessage message) { 11 | Gson gson = new Gson(); 12 | return gson.toJson(message); 13 | } 14 | 15 | @Override 16 | public void init(EndpointConfig config) { 17 | 18 | } 19 | 20 | @Override 21 | public void destroy() { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /xmpp-react-client/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import contactsReducer from "../features/contacts/contactsSlice"; 3 | import currentReducer from "../features/current/currentSlice"; 4 | import messagesReducer from "../features/messages/messagesSlice"; 5 | import alertReducer from "../features/alert/alertSlice"; 6 | import userReducer from "../features/user/userSlice"; 7 | import websocketMiddleware from "../common/middleware/websocketMiddleware"; 8 | 9 | export const store = configureStore({ 10 | reducer: { 11 | user: userReducer, 12 | alert: alertReducer, 13 | contacts: contactsReducer, 14 | current: currentReducer, 15 | messages: messagesReducer, 16 | }, 17 | middleware: (getDefaultMiddleware) => 18 | getDefaultMiddleware().concat(websocketMiddleware), 19 | }); 20 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/config/SpringContext.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.config; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class SpringContext implements ApplicationContextAware { 10 | 11 | private static ApplicationContext context; 12 | 13 | @Override 14 | public void setApplicationContext(ApplicationContext context) throws BeansException { 15 | this.context = context; 16 | } 17 | 18 | public static ApplicationContext getApplicationContext() { 19 | return context; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /xmpp-react-client/src/index.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { Provider } from "react-redux"; 5 | import "../node_modules/font-awesome/css/font-awesome.min.css"; 6 | import App from "./app/App"; 7 | import { store } from "./app/store"; 8 | import "./index.css"; 9 | import * as serviceWorker from "./serviceWorker"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: https://bit.ly/CRA-PWA 23 | serviceWorker.unregister(); 24 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.service; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.repository.AccountRepository; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.Account; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.Optional; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class AccountService { 13 | 14 | private final AccountRepository accountRepository; 15 | 16 | public Optional getAccount(String username) { 17 | return accountRepository.findById(username); 18 | } 19 | 20 | public void saveAccount(Account account) { 21 | accountRepository.save(account); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/xmpp/XMPPProperties.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.xmpp; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | /** 8 | * A connection contains common information needed to connect to an XMPP server 9 | * and sign in. 10 | */ 11 | @Getter 12 | @Setter 13 | @ConfigurationProperties(prefix = "xmpp") 14 | public class XMPPProperties { 15 | 16 | /** 17 | * The address of the server. 18 | */ 19 | private String host; 20 | 21 | /** 22 | * The port to use (usually 5222). 23 | */ 24 | private int port; 25 | 26 | /** 27 | * The XMPP domain is what follows after the '@' sign in XMPP addresses (JIDs). 28 | */ 29 | private String domain; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/websocket/utils/MessageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils; 2 | 3 | import com.google.gson.Gson; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 5 | import jakarta.websocket.Decoder; 6 | import jakarta.websocket.EndpointConfig; 7 | 8 | public class MessageDecoder implements Decoder.Text { 9 | 10 | @Override 11 | public WebsocketMessage decode(String message) { 12 | Gson gson = new Gson(); 13 | return gson.fromJson(message, WebsocketMessage.class); 14 | } 15 | 16 | @Override 17 | public boolean willDecode(String message) { 18 | return (message != null); 19 | } 20 | 21 | @Override 22 | public void init(EndpointConfig config) { 23 | 24 | } 25 | 26 | @Override 27 | public void destroy() { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/websocket/utils/WebSocketTextMessageHelper.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 4 | import jakarta.websocket.EncodeException; 5 | import jakarta.websocket.Session; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.io.IOException; 10 | 11 | @Slf4j 12 | @Component 13 | public class WebSocketTextMessageHelper { 14 | 15 | public void send(Session session, WebsocketMessage websocketMessage) { 16 | try { 17 | log.info("Sending message of type '{}'.", websocketMessage.getMessageType()); 18 | session.getBasicRemote().sendObject(websocketMessage); 19 | } catch (IOException | EncodeException e) { 20 | log.error("WebSocket error, message {} was not sent.", websocketMessage, e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/Contact.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { selectCurrent } from "../current/currentSlice"; 4 | 5 | 6 | 7 | const Contact = ({ select, name, eventKey }) => { 8 | const setCurrent = () => { 9 | select(name); 10 | }; 11 | 12 | const current = useSelector(selectCurrent); 13 | 14 | return ( 15 |
  • 20 | avatar 24 |
    25 |
    {name}
    26 |
    27 | left 7 mins ago 28 |
    29 | {/*
    online
    */} 30 |
    31 |
  • 32 | ); 33 | }; 34 | 35 | export default Contact; 36 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.config; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.ChatWebSocket; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 10 | 11 | @Configuration 12 | @EnableWebSocket 13 | public class WebSocketConfig implements WebSocketConfigurer { 14 | 15 | @Override 16 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 17 | } 18 | 19 | @Bean 20 | public ChatWebSocket chatWebSocket() { 21 | return new ChatWebSocket(); 22 | } 23 | 24 | @Bean 25 | public ServerEndpointExporter serverEndpointExporter() { 26 | return new ServerEndpointExporter(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | xmpp-server: 3 | container_name: spring-xmpp-websocket-server 4 | image: spring-xmpp-websocket-server 5 | ports: 6 | - "8080:8080" 7 | depends_on: 8 | - spring-postgres 9 | - openfire 10 | spring-postgres: 11 | container_name: spring-postgres 12 | image: "postgres:14.6" 13 | ports: 14 | - "5432:5432" 15 | environment: 16 | POSTGRES_USER: xmpp 17 | POSTGRES_PASSWORD: password 18 | POSTGRES_DB: chat 19 | openfire-mysql: 20 | container_name: openfire-mysql 21 | image: mysql/mysql-server:latest 22 | ports: 23 | - "3306:3306" 24 | environment: 25 | MYSQL_DATABASE: openfire 26 | MYSQL_USER: openfireuser 27 | MYSQL_PASSWORD: openfirepasswd 28 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 29 | openfire: 30 | container_name: openfire 31 | image: nasqueron/openfire:4.7.5 32 | ports: 33 | - "9090:9090" 34 | - "5222:5222" 35 | - "5269:5269" 36 | - "5223:5223" 37 | - "7443:7443" 38 | - "7777:7777" 39 | - "7070:7070" 40 | - "5229:5229" 41 | - "5275:5275" 42 | depends_on: 43 | - openfire-mysql 44 | -------------------------------------------------------------------------------- /xmpp-react-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmpp-react-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.5.1", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "bootstrap": "^4.6.0", 11 | "font-awesome": "4.7.0", 12 | "history": "^4.10.1", 13 | "react": "^17.0.2", 14 | "react-bootstrap": "^1.6.0", 15 | "react-dom": "^17.0.2", 16 | "react-redux": "^7.2.4", 17 | "react-router": "^5.2.0", 18 | "react-router-dom": "^5.2.0", 19 | "react-scripts": "4.0.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts --openssl-legacy-provider start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /xmpp-react-client/src/app/Router.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { Router } from "react-router"; 4 | import { Redirect, Route, Switch } from "react-router-dom"; 5 | import { history } from "./browserhistory"; 6 | import HomeContainer from "../features/home/HomeContainer"; 7 | import LoginContainer from "../features/user/LoginContainer"; 8 | import { selectLoggedIn } from "../features/user/userSlice"; 9 | 10 | const MyRouter = () => { 11 | const loggedIn = useSelector(selectLoggedIn); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | {loggedIn ? : } 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | const PrivateRoute = ({ component: Component, ...args }) => { 29 | const loggedIn = useSelector(selectLoggedIn); 30 | 31 | return ( 32 | 33 | {loggedIn === true ? : } 34 | 35 | ); 36 | }; 37 | 38 | export default MyRouter; 39 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/contacts/AddContactBox.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { addContact } from "./contactActions"; 3 | 4 | const AddContactBox = ({ dispatch }) => { 5 | const [contact, setContact] = useState(""); 6 | 7 | const validateForm = () => { 8 | return contact.length > 0; 9 | }; 10 | 11 | const handleSubmit = (e) => { 12 | e.preventDefault(); 13 | const msg = { 14 | to: contact, 15 | messageType: "ADD_CONTACT", 16 | }; 17 | setContact(""); 18 | dispatch(addContact(msg)); 19 | }; 20 | 21 | return ( 22 |
    handleSubmit(e)}> 23 |
    24 |
    25 | 32 |
    33 | setContact(e.target.value)} 39 | /> 40 |
    41 |
    42 | ); 43 | }; 44 | 45 | export default AddContactBox; 46 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/Message.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Message = ({ id, content, type }) => { 4 | let dateClassName = "message-data text-right"; 5 | let contentClassName = "message other-message float-right"; 6 | if (type === "NEW_MESSAGE") { 7 | dateClassName = "message-data"; 8 | contentClassName = "message my-message"; 9 | } 10 | const months = [ 11 | "Jan", 12 | "Feb", 13 | "Mar", 14 | "Apr", 15 | "May", 16 | "Jun", 17 | "Jul", 18 | "Aug", 19 | "Sep", 20 | "Oct", 21 | "Nov", 22 | "Dec", 23 | ]; 24 | const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 25 | 26 | var today = new Date(); 27 | let minutes = 28 | today.getMinutes() < 10 ? "0" + today.getMinutes() : today.getMinutes(); 29 | let hours = today.getHours() < 10 ? "0" + today.getHours() : today.getHours(); 30 | let time = hours + ":" + minutes; 31 | let date = 32 | days[today.getDay()] + 33 | " " + 34 | today.getDate() + 35 | " " + 36 | months[today.getMonth()]; 37 | 38 | return ( 39 |
  • 40 |
    41 | 42 | {time}, {date} 43 | 44 |
    45 |
    {content}
    46 |
  • 47 | ); 48 | }; 49 | 50 | export default Message; 51 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/home/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Navbar } from "react-bootstrap"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { wsDisconnect } from "../../common/middleware/websocketActions"; 5 | import { ContactsBoxContainer } from "../contacts/ContactsBoxContainer"; 6 | import { ChatBoxContainer } from "../messages/ChatBoxContainer"; 7 | import { selectUsername } from "../user/userSlice"; 8 | 9 | const Home = () => { 10 | const user = useSelector(selectUsername); 11 | 12 | const capitalize = (text) => { 13 | return text.charAt(0).toUpperCase() + text.slice(1); 14 | }; 15 | 16 | const dispatch = useDispatch(); 17 | 18 | const handleLogout = (e) => { 19 | e.preventDefault(); 20 | dispatch(wsDisconnect()); 21 | }; 22 | 23 | return ( 24 |
    25 | 26 | Welcome {capitalize(user.username)} 27 | 28 | 31 | 32 | 33 | 34 |
    35 |
    36 |
    37 |
    38 | 39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | ); 46 | }; 47 | 48 | export default Home; 49 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/xmpp/XMPPMessageTransmitter.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.xmpp; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils.WebSocketTextMessageHelper; 5 | import jakarta.websocket.Session; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.jivesoftware.smack.packet.Message; 9 | import org.springframework.stereotype.Component; 10 | 11 | import static com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType.NEW_MESSAGE; 12 | 13 | @Slf4j 14 | @Component 15 | @RequiredArgsConstructor 16 | public class XMPPMessageTransmitter { 17 | 18 | private final WebSocketTextMessageHelper webSocketTextMessageHelper; 19 | 20 | public void sendResponse(Message message, Session session) { 21 | log.info("New message from '{}' to '{}': {}", message.getFrom(), message.getTo(), message.getBody()); 22 | String messageFrom = message.getFrom().getLocalpartOrNull().toString(); 23 | String to = message.getTo().getLocalpartOrNull().toString(); 24 | String content = message.getBody(); 25 | webSocketTextMessageHelper.send( 26 | session, 27 | WebsocketMessage.builder() 28 | .from(messageFrom) 29 | .to(to) 30 | .content(content) 31 | .messageType(NEW_MESSAGE).build() 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /xmpp-react-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Redux App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-xmpp-websocket-reactjs 2 | 3 | ![IM System Diagram](im-system-diagram.jpg) 4 | 5 | ## Tech Stack 6 | 7 | - **Spring Boot** 8 | - [Smack](https://www.igniterealtime.org/projects/smack/) 9 | - **Websocket** 10 | - **[MySQL](https://sergiomartinrubio.com/articles/mysql-guide/)** 11 | - **[Liquibase](https://sergiomartinrubio.com/articles/getting-started-with-liquibase-and-spring-boot/)** 12 | - **[BCrypt](https://sergiomartinrubio.com/articles/storing-passwords-securely-with-bcrypt-and-java/)** 13 | 14 | ## Installation 15 | 16 | 1. Run backend services: 17 | ```shell 18 | mvn clean install 19 | docker build -t spring-xmpp-websocket-server . 20 | docker-compose up 21 | ``` 22 | 2. Go to `http://localhost:9090` and setup openfire XMPP server: 23 | - Server settings: 24 | - Set "XMPP Domain Name" to `localhost` 25 | - Set "Server Host Name (FQDN)" to `localhost` 26 | - Leave the rest as it is. 27 | - Database Settings: 28 | - Select "Standard Database Connection" 29 | - Select "MySQL" 30 | - Replace on the "Database URL" `HOSTNAME` with `openfire-mysql` and `DATABASENAME` with `openfire`, then fill in the username and password. 31 | - Continue and ignore the rest of the steps. 32 | 3. Now you can use a websocket client to try out the backend application. 33 | - Endpoint: ws://localhost:8080/chat/sergio/pass 34 | - Connect will return `{"messageType":"JOIN_SUCCESS"}` 35 | - Send new message with body: 36 | ``` 37 | { 38 | "from": "sergio", 39 | "to": "jose", 40 | "content": "hello world", 41 | "messageType": "NEW_MESSAGE" 42 | } 43 | ``` 44 | will return `{"from":"sergio","to":"jose","content":"hello world","messageType":"NEW_MESSAGE"}` 45 | 46 | 4. Run ReactJS App 47 | 48 | ```shell 49 | npm install 50 | npm start 51 | ``` 52 | 53 | ## Running Tests 54 | 55 | To run tests, run the following command 56 | 57 | ```bash 58 | mvn clean install 59 | ``` 60 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/ChatBox.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { selectCurrent } from "../current/currentSlice"; 4 | import Message from "./Message"; 5 | import { selectMessages } from "./messagesSlice"; 6 | import { TypingAreaContainer } from "./TypingAreaContainer"; 7 | import { unsubscribe } from "./typingAreaActions.js"; 8 | 9 | const ChatBox = () => { 10 | const messages = useSelector(selectMessages); 11 | 12 | const ref = React.useRef(); 13 | useEffect(() => { 14 | if (ref.current) { 15 | ref.current.scrollIntoView({ behavior: "smooth", block: "end" }); 16 | } 17 | }, [messages]); 18 | 19 | const currentContact = useSelector(selectCurrent); 20 | 21 | const dispatch = useDispatch(); 22 | 23 | const handleSubmit = (e) => { 24 | e.preventDefault(); 25 | const msg = { 26 | to: currentContact, 27 | messageType: "UNSUBSCRIBE", 28 | }; 29 | dispatch(unsubscribe(msg)); 30 | }; 31 | 32 | return ( 33 |
    34 |
    35 |
    36 |
    37 | avatar 41 |
    42 |
    {currentContact}
    43 | Last seen: 2 hours ago 44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
      51 | {messages.map((message) => ( 52 |
      53 | 54 |
      55 | ))} 56 |
    57 |
    58 | 59 |
    60 | ); 61 | }; 62 | 63 | export default ChatBox; 64 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/websocket/ChatWebSocket.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.websocket; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.config.SpringContext; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.facade.XMPPFacade; 5 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 6 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils.MessageDecoder; 7 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils.MessageEncoder; 8 | import jakarta.websocket.*; 9 | import jakarta.websocket.server.PathParam; 10 | import jakarta.websocket.server.ServerEndpoint; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | 14 | @Slf4j 15 | @ServerEndpoint(value = "/chat/{username}/{password}", decoders = MessageDecoder.class, encoders = MessageEncoder.class) 16 | public class ChatWebSocket { 17 | 18 | private final XMPPFacade xmppFacade; 19 | 20 | public ChatWebSocket() { 21 | this.xmppFacade = (XMPPFacade) SpringContext.getApplicationContext().getBean("XMPPFacade"); 22 | } 23 | 24 | @OnOpen 25 | public void open(Session session, @PathParam("username") String username, @PathParam("password") String password) { 26 | log.info("Starting XMPP session '{}'.", session.getId()); 27 | xmppFacade.startSession(session, username, password); 28 | } 29 | 30 | @OnMessage 31 | public void handleMessage(WebsocketMessage message, Session session) { 32 | log.info("Sending message for session '{}'.", session.getId()); 33 | xmppFacade.sendMessage(message, session); 34 | log.info("Message sent for session '{}'.", session.getId()); 35 | } 36 | 37 | @OnClose 38 | public void close(Session session) { 39 | xmppFacade.disconnect(session); 40 | } 41 | 42 | @OnError 43 | public void onError(Throwable e, Session session) { 44 | log.warn("Something went wrong.", e); 45 | xmppFacade.disconnect(session); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/messages/TypingArea.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React, { useState } from "react"; 3 | import { useSelector } from "react-redux"; 4 | import { selectCurrent } from "../current/currentSlice"; 5 | import { selectUsername } from "../user/userSlice"; 6 | import { addMessage } from "./messagesSlice"; 7 | import { newMessage } from "./typingAreaActions"; 8 | 9 | const TypingArea = ({ dispatch }) => { 10 | const [content, setContent] = useState(""); 11 | 12 | const user = useSelector(selectUsername); 13 | 14 | const currentContact = useSelector(selectCurrent); 15 | 16 | const handleMessage = () => { 17 | const msg = { 18 | from: user.username, 19 | to: currentContact, 20 | content: content, 21 | messageType: "NEW_MESSAGE", 22 | }; 23 | const msgSent = { 24 | from: user.username, 25 | to: currentContact, 26 | content: content, 27 | }; 28 | setContent(""); 29 | dispatch(addMessage(msgSent)); 30 | dispatch(newMessage(msg)); 31 | }; 32 | 33 | const onKeyDown = (event) => { 34 | if (event.key === "Enter") { 35 | handleMessage(); 36 | } 37 | }; 38 | 39 | const validateForm = () => { 40 | return content.length > 0; 41 | }; 42 | 43 | return ( 44 |
    45 |
    46 |
    47 | 55 |
    56 | setContent(e.target.value)} 61 | onKeyDown={onKeyDown} 62 | placeholder="Enter text here..." 63 | /> 64 |
    65 |
    66 | ); 67 | }; 68 | 69 | TypingArea.propTypes = { 70 | dispatch: PropTypes.func.isRequired, 71 | }; 72 | 73 | export default TypingArea; 74 | -------------------------------------------------------------------------------- /xmpp-react-client/src/features/user/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Alert, 4 | Button, 5 | Container, 6 | FormControl, 7 | FormGroup, 8 | FormLabel, 9 | Jumbotron, 10 | } from "react-bootstrap"; 11 | import { useSelector } from "react-redux"; 12 | import { wsConnect } from "../../common/middleware/websocketActions"; 13 | import { selectAlert } from "../alert/alertSlice"; 14 | 15 | // use rfce to generate component quickly 16 | const Login = ({ dispatch }) => { 17 | const [username, setUsername] = useState(""); 18 | const [password, setPassword] = useState(""); 19 | 20 | const validateForm = () => { 21 | return username.length > 0 && password.length > 0; 22 | }; 23 | 24 | const handleSubmit = (e) => { 25 | e.preventDefault(); 26 | dispatch(wsConnect(username, password)); 27 | }; 28 | 29 | const alert = useSelector(selectAlert); 30 | 31 | return ( 32 |
    33 | 34 | 35 |

    Login Here 🚪

    36 |
    37 |
    38 | 39 |
    47 | {alert ? ( 48 | {alert.message} 49 | ) : ( 50 |
    51 | )} 52 |
    53 |
    handleSubmit(e)}> 54 | 55 | Username 56 | setUsername(e.target.value)} 62 | /> 63 | 64 | 65 | Password 66 | setPassword(e.target.value)} 72 | /> 73 | 74 | 77 |
    78 |
    79 |
    80 | ); 81 | }; 82 | 83 | export default Login; 84 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.5 9 | 10 | 11 | com.sergiomartinrubio 12 | spring-xmpp-websocket-server 13 | 0.1.0 14 | spring-xmpp-websocket-security 15 | Demo project for Spring Boot 16 | 17 | 18 | 21 19 | 4.4.6 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-websocket 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-data-jpa 30 | 31 | 32 | org.postgresql 33 | postgresql 34 | runtime 35 | 36 | 37 | org.liquibase 38 | liquibase-core 39 | 4.23.0 40 | 41 | 42 | 43 | org.springframework.security 44 | spring-security-crypto 45 | 46 | 47 | 48 | org.projectlombok 49 | lombok 50 | provided 51 | 52 | 53 | 54 | 55 | org.igniterealtime.smack 56 | smack-core 57 | ${smack.version} 58 | 59 | 60 | org.igniterealtime.smack 61 | smack-tcp 62 | ${smack.version} 63 | 64 | 65 | org.igniterealtime.smack 66 | smack-java7 67 | 4.4.0-beta2 68 | 69 | 70 | org.igniterealtime.smack 71 | smack-extensions 72 | ${smack.version} 73 | 74 | 75 | org.igniterealtime.smack 76 | smack-im 77 | ${smack.version} 78 | 79 | 80 | 81 | com.google.code.gson 82 | gson 83 | 2.10.1 84 | 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-test 89 | test 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-maven-plugin 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /xmpp-react-client/src/common/middleware/websocketMiddleware.js: -------------------------------------------------------------------------------- 1 | import { history } from "../../app/browserhistory"; 2 | import { disableAlert, enableAlert } from "../../features/alert/alertSlice"; 3 | import { add } from "../../features/contacts/contactsSlice"; 4 | import { addMessage } from "../../features/messages/messagesSlice"; 5 | import { login, logout } from "../../features/user/userSlice"; 6 | 7 | const websocketMiddleware = () => { 8 | let socket = null; 9 | 10 | const onOpen = (store) => (event) => { 11 | // store.dispatch(actions.wsConnected(event.target.url)); 12 | }; 13 | 14 | const onClose = (store) => () => { 15 | store.dispatch(logout()); 16 | history.push("/login"); 17 | }; 18 | 19 | const onMessage = (store) => (event) => { 20 | const payload = JSON.parse(event.data); 21 | switch (payload.messageType) { 22 | case "JOIN_SUCCESS": 23 | store.dispatch( 24 | login({ 25 | username: payload.to, 26 | loggedIn: true, 27 | }) 28 | ); 29 | 30 | store.dispatch(disableAlert()); 31 | 32 | history.push("/home"); 33 | 34 | const msg = { 35 | messageType: "GET_CONTACTS", 36 | }; 37 | 38 | socket.send(JSON.stringify(msg)); 39 | break; 40 | case "NEW_MESSAGE": 41 | const message = { 42 | content: payload.content, 43 | type: payload.messageType, 44 | }; 45 | store.dispatch(addMessage(message)); 46 | break; 47 | case "ERROR": 48 | store.dispatch(logout()); 49 | history.push("/login"); 50 | socket = null; 51 | break; 52 | case "LEAVE": 53 | console.log(payload); 54 | socket = null; 55 | break; 56 | case "FORBIDDEN": 57 | store.dispatch( 58 | enableAlert({ message: "Invalid password", enabled: true }) 59 | ); 60 | console.log("Invalid password"); 61 | break; 62 | case "GET_CONTACTS": 63 | store.dispatch(add(JSON.parse(payload.content))); 64 | break; 65 | default: 66 | console.log(payload); 67 | break; 68 | } 69 | }; 70 | 71 | const onError = (store) => (event) => { 72 | const msg = { 73 | message: 74 | "Something went wrong when connecting to the Chat Server. Please contact support if the error persists.", 75 | enabled: true, 76 | }; 77 | store.dispatch(enableAlert(msg)); 78 | }; 79 | 80 | return (store) => (next) => (action) => { 81 | switch (action.type) { 82 | case "WS_CONNECT": 83 | if (socket !== null) { 84 | socket.close(); 85 | } 86 | 87 | socket = new WebSocket( 88 | "ws://localhost:8080/chat/" + action.username + "/" + action.password 89 | ); 90 | 91 | // websocket handlers 92 | socket.onmessage = onMessage(store); 93 | socket.onclose = onClose(store); 94 | socket.onopen = onOpen(store); 95 | socket.onerror = onError(store); 96 | 97 | break; 98 | case "WS_DISCONNECT": 99 | if (socket !== null) { 100 | socket.close(); 101 | } 102 | socket = null; 103 | store.dispatch(logout()); 104 | history.push("/login"); 105 | break; 106 | case "NEW_MESSAGE": 107 | socket.send(JSON.stringify(action.msg)); 108 | break; 109 | case 'UNSUBSCRIBE': 110 | socket.send(JSON.stringify(action.msg)); 111 | break; 112 | case "ADD_CONTACT": 113 | socket.send(JSON.stringify(action.msg)); 114 | 115 | const msg = { 116 | messageType: "GET_CONTACTS", 117 | }; 118 | 119 | socket.send(JSON.stringify(msg)); 120 | break; 121 | default: 122 | return next(action); 123 | } 124 | }; 125 | }; 126 | 127 | export default websocketMiddleware(); 128 | -------------------------------------------------------------------------------- /xmpp-react-client/src/index.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: #f4f7f6; 3 | margin-top:20px; 4 | } 5 | .card { 6 | background: #fff; 7 | transition: .5s; 8 | border: 0; 9 | margin-bottom: 30px; 10 | border-radius: .55rem; 11 | position: relative; 12 | width: 100%; 13 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 10%); 14 | } 15 | .chat-app .people-list { 16 | width: 280px; 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | padding: 20px; 21 | z-index: 7 22 | } 23 | 24 | .chat-app .chat { 25 | margin-left: 280px; 26 | border-left: 1px solid #eaeaea 27 | } 28 | 29 | .people-list { 30 | -moz-transition: .5s; 31 | -o-transition: .5s; 32 | -webkit-transition: .5s; 33 | transition: .5s 34 | } 35 | 36 | .people-list .chat-list li { 37 | padding: 10px 15px; 38 | list-style: none; 39 | border-radius: 3px 40 | } 41 | 42 | .people-list .chat-list li:hover { 43 | background: #efefef; 44 | cursor: pointer 45 | } 46 | 47 | .people-list .chat-list li.active { 48 | background: #efefef 49 | } 50 | 51 | .people-list .chat-list li .name { 52 | font-size: 15px 53 | } 54 | 55 | .people-list .chat-list img { 56 | width: 45px; 57 | border-radius: 50% 58 | } 59 | 60 | .people-list img { 61 | float: left; 62 | border-radius: 50% 63 | } 64 | 65 | .people-list .about { 66 | float: left; 67 | padding-left: 8px 68 | } 69 | 70 | .people-list .status { 71 | color: #999; 72 | font-size: 13px 73 | } 74 | 75 | .chat .chat-header { 76 | padding: 15px 20px; 77 | border-bottom: 2px solid #f4f7f6 78 | } 79 | 80 | .chat .chat-header img { 81 | float: left; 82 | border-radius: 40px; 83 | width: 40px 84 | } 85 | 86 | .chat .chat-header .chat-about { 87 | float: left; 88 | padding-left: 10px 89 | } 90 | 91 | .chat .chat-history { 92 | padding: 20px; 93 | border-bottom: 2px solid #fff 94 | } 95 | 96 | .chat .chat-history ul { 97 | padding: 0 98 | } 99 | 100 | .chat .chat-history ul li { 101 | list-style: none; 102 | margin-bottom: 30px 103 | } 104 | 105 | .chat .chat-history ul li:last-child { 106 | margin-bottom: 0px 107 | } 108 | 109 | .chat .chat-history .message-data { 110 | margin-bottom: 15px 111 | } 112 | 113 | .chat .chat-history .message-data img { 114 | border-radius: 40px; 115 | width: 40px 116 | } 117 | 118 | .chat .chat-history .message-data-time { 119 | color: #434651; 120 | padding-left: 6px 121 | } 122 | 123 | .chat .chat-history .message { 124 | color: #444; 125 | padding: 18px 20px; 126 | line-height: 26px; 127 | font-size: 16px; 128 | border-radius: 7px; 129 | display: inline-block; 130 | position: relative 131 | } 132 | 133 | .chat .chat-history .message:after { 134 | bottom: 100%; 135 | left: 7%; 136 | border: solid transparent; 137 | content: " "; 138 | height: 0; 139 | width: 0; 140 | position: absolute; 141 | pointer-events: none; 142 | border-bottom-color: #fff; 143 | border-width: 10px; 144 | margin-left: -10px 145 | } 146 | 147 | .chat .chat-history .my-message { 148 | background: #efefef 149 | } 150 | 151 | .chat .chat-history .my-message:after { 152 | bottom: 100%; 153 | left: 30px; 154 | border: solid transparent; 155 | content: " "; 156 | height: 0; 157 | width: 0; 158 | position: absolute; 159 | pointer-events: none; 160 | border-bottom-color: #efefef; 161 | border-width: 10px; 162 | margin-left: -10px 163 | } 164 | 165 | .chat .chat-history .other-message { 166 | background: #e8f1f3; 167 | text-align: right 168 | } 169 | 170 | .chat .chat-history .other-message:after { 171 | border-bottom-color: #e8f1f3; 172 | left: 93% 173 | } 174 | 175 | .chat .chat-message { 176 | padding: 20px 177 | } 178 | 179 | .online, 180 | .offline, 181 | .me { 182 | margin-right: 2px; 183 | font-size: 8px; 184 | vertical-align: middle 185 | } 186 | 187 | .online { 188 | color: #86c541 189 | } 190 | 191 | .offline { 192 | color: #e47297 193 | } 194 | 195 | .me { 196 | color: #1d8ecd 197 | } 198 | 199 | .float-right { 200 | float: right 201 | } 202 | 203 | .clearfix:after { 204 | visibility: hidden; 205 | display: block; 206 | font-size: 0; 207 | content: " "; 208 | clear: both; 209 | height: 0 210 | } 211 | 212 | @media only screen and (max-width: 767px) { 213 | .chat-app .people-list { 214 | height: 465px; 215 | width: 100%; 216 | overflow-x: auto; 217 | background: #fff; 218 | left: -400px; 219 | display: none 220 | } 221 | .chat-app .people-list.open { 222 | left: 0 223 | } 224 | .chat-app .chat { 225 | margin: 0 226 | } 227 | .chat-app .chat .chat-header { 228 | border-radius: 0.55rem 0.55rem 0 0 229 | } 230 | .chat-app .chat-history { 231 | height: 300px; 232 | overflow-x: auto 233 | } 234 | } 235 | 236 | @media only screen and (min-width: 768px) and (max-width: 992px) { 237 | .chat-app .chat-list { 238 | height: 650px; 239 | overflow-x: auto 240 | } 241 | .chat-app .chat-history { 242 | height: 600px; 243 | overflow-x: auto 244 | } 245 | } 246 | 247 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1) { 248 | .chat-app .chat-list { 249 | height: 480px; 250 | overflow-x: auto 251 | } 252 | .chat-app .chat-history { 253 | height: calc(100vh - 350px); 254 | overflow-x: auto 255 | } 256 | } -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /xmpp-react-client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then((registration) => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/facade/XMPPFacade.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.facade; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.exception.XMPPGenericException; 5 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.Account; 6 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 7 | import com.sergiomartinrubio.springxmppwebsocketsecurity.service.AccountService; 8 | import com.sergiomartinrubio.springxmppwebsocketsecurity.utils.BCryptUtils; 9 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils.WebSocketTextMessageHelper; 10 | import com.sergiomartinrubio.springxmppwebsocketsecurity.xmpp.XMPPClient; 11 | import jakarta.websocket.Session; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.jivesoftware.smack.packet.Presence; 15 | import org.jivesoftware.smack.roster.RosterEntry; 16 | import org.jivesoftware.smack.tcp.XMPPTCPConnection; 17 | import org.springframework.stereotype.Component; 18 | 19 | import java.io.IOException; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | import java.util.Optional; 23 | import java.util.Set; 24 | 25 | import static com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType.*; 26 | 27 | @Slf4j 28 | @Component 29 | @RequiredArgsConstructor 30 | public class XMPPFacade { 31 | 32 | private static final Map CONNECTIONS = new HashMap<>(); 33 | 34 | private final AccountService accountService; 35 | private final WebSocketTextMessageHelper webSocketTextMessageHelper; 36 | private final XMPPClient xmppClient; 37 | 38 | public void startSession(Session session, String username, String password) { 39 | // TODO: Save user session to avoid having to login again when the websocket connection is closed 40 | // 1. Generate token 41 | // 2. Save username and token in Redis 42 | // 3. Return token to client and store it in a cookie or local storage 43 | // 4. When starting a websocket session check if the token is still valid and bypass XMPP authentication 44 | Optional account = accountService.getAccount(username); 45 | 46 | if (account.isPresent() && !BCryptUtils.isMatch(password, account.get().getPassword())) { 47 | log.warn("Invalid password for user {}.", username); 48 | webSocketTextMessageHelper.send(session, WebsocketMessage.builder().messageType(FORBIDDEN).build()); 49 | return; 50 | } 51 | 52 | Optional connection = xmppClient.connect(username, password); 53 | 54 | if (connection.isEmpty()) { 55 | log.info("XMPP connection was not established. Closing websocket session..."); 56 | webSocketTextMessageHelper.send(session, WebsocketMessage.builder().messageType(ERROR).build()); 57 | try { 58 | session.close(); 59 | return; 60 | } catch (IOException e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | try { 66 | if (account.isEmpty()) { 67 | log.info("Creating new account."); 68 | xmppClient.createAccount(connection.get(), username, password); 69 | log.info("Account created."); 70 | } 71 | log.info("Login into account."); 72 | xmppClient.login(connection.get()); 73 | } catch (XMPPGenericException e) { 74 | handleXMPPGenericException(session, connection.get(), e); 75 | return; 76 | } 77 | 78 | CONNECTIONS.put(session, connection.get()); 79 | log.info("Session was stored."); 80 | 81 | xmppClient.addIncomingMessageListener(connection.get(), session); 82 | 83 | webSocketTextMessageHelper.send(session, WebsocketMessage.builder().to(username).messageType(JOIN_SUCCESS).build()); 84 | } 85 | 86 | public void sendMessage(WebsocketMessage message, Session session) { 87 | XMPPTCPConnection connection = CONNECTIONS.get(session); 88 | 89 | if (connection == null) { 90 | return; 91 | } 92 | 93 | switch (message.getMessageType()) { 94 | case NEW_MESSAGE -> { 95 | try { 96 | xmppClient.sendMessage(connection, message.getContent(), message.getTo()); 97 | // TODO: save message for both users in DB 98 | } catch (XMPPGenericException e) { 99 | handleXMPPGenericException(session, connection, e); 100 | } 101 | } 102 | case ADD_CONTACT -> { 103 | try { 104 | xmppClient.addContact(connection, message.getTo()); 105 | } catch (XMPPGenericException e) { 106 | handleXMPPGenericException(session, connection, e); 107 | } 108 | } 109 | case UNSUBSCRIBE -> { 110 | try { 111 | xmppClient.remove(connection, message.getTo()); 112 | } catch (XMPPGenericException e) { 113 | handleXMPPGenericException(session, connection, e); 114 | } 115 | } 116 | case GET_CONTACTS -> { 117 | Set contacts = Set.of(); 118 | try { 119 | contacts = xmppClient.getContacts(connection); 120 | } catch (XMPPGenericException e) { 121 | handleXMPPGenericException(session, connection, e); 122 | } 123 | 124 | JsonArray jsonArray = new JsonArray(); 125 | for (RosterEntry entry : contacts) { 126 | jsonArray.add(entry.getName()); 127 | } 128 | WebsocketMessage responseMessage = WebsocketMessage.builder() 129 | .content(jsonArray.toString()) 130 | .messageType(GET_CONTACTS) 131 | .build(); 132 | log.info("Returning list of contacts {} for user {}.", jsonArray, connection.getUser()); 133 | webSocketTextMessageHelper.send(session, responseMessage); 134 | } 135 | default -> log.warn("Message type not implemented."); 136 | } 137 | } 138 | 139 | public void disconnect(Session session) { 140 | XMPPTCPConnection connection = CONNECTIONS.get(session); 141 | 142 | if (connection == null) { 143 | return; 144 | } 145 | 146 | try { 147 | xmppClient.sendStanza(connection, Presence.Type.unavailable); 148 | } catch (XMPPGenericException e) { 149 | log.error("XMPP error.", e); 150 | webSocketTextMessageHelper.send(session, WebsocketMessage.builder().messageType(ERROR).build()); 151 | } 152 | 153 | xmppClient.disconnect(connection); 154 | CONNECTIONS.remove(session); 155 | } 156 | 157 | private void handleXMPPGenericException(Session session, XMPPTCPConnection connection, Exception e) { 158 | log.error("XMPP error. Disconnecting and removing session...", e); 159 | xmppClient.disconnect(connection); 160 | webSocketTextMessageHelper.send(session, WebsocketMessage.builder().messageType(ERROR).build()); 161 | CONNECTIONS.remove(session); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/main/java/com/sergiomartinrubio/springxmppwebsocketsecurity/xmpp/XMPPClient.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.xmpp; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.exception.XMPPGenericException; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.Account; 5 | import com.sergiomartinrubio.springxmppwebsocketsecurity.service.AccountService; 6 | import com.sergiomartinrubio.springxmppwebsocketsecurity.utils.BCryptUtils; 7 | import jakarta.websocket.Session; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.jivesoftware.smack.ConnectionConfiguration; 11 | import org.jivesoftware.smack.SmackException; 12 | import org.jivesoftware.smack.XMPPException; 13 | import org.jivesoftware.smack.chat2.Chat; 14 | import org.jivesoftware.smack.chat2.ChatManager; 15 | import org.jivesoftware.smack.packet.Presence; 16 | import org.jivesoftware.smack.packet.PresenceBuilder; 17 | import org.jivesoftware.smack.roster.Roster; 18 | import org.jivesoftware.smack.roster.RosterEntry; 19 | import org.jivesoftware.smack.tcp.XMPPTCPConnection; 20 | import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; 21 | import org.jivesoftware.smackx.iqregister.AccountManager; 22 | import org.jxmpp.jid.BareJid; 23 | import org.jxmpp.jid.EntityBareJid; 24 | import org.jxmpp.jid.EntityFullJid; 25 | import org.jxmpp.jid.impl.JidCreate; 26 | import org.jxmpp.jid.parts.Localpart; 27 | import org.jxmpp.stringprep.XmppStringprepException; 28 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 29 | import org.springframework.stereotype.Component; 30 | 31 | import java.io.IOException; 32 | import java.util.Optional; 33 | import java.util.Set; 34 | 35 | @Slf4j 36 | @Component 37 | @RequiredArgsConstructor 38 | @EnableConfigurationProperties(XMPPProperties.class) 39 | public class XMPPClient { 40 | 41 | private final XMPPProperties xmppProperties; 42 | private final AccountService accountService; 43 | private final XMPPMessageTransmitter xmppMessageTransmitter; 44 | 45 | public Optional connect(String username, String plainTextPassword) { 46 | XMPPTCPConnection connection; 47 | try { 48 | EntityBareJid entityBareJid; 49 | entityBareJid = JidCreate.entityBareFrom(username + "@" + xmppProperties.getDomain()); 50 | XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder() 51 | .setHost(xmppProperties.getHost()) 52 | .setPort(xmppProperties.getPort()) 53 | .setXmppDomain(xmppProperties.getDomain()) 54 | .setUsernameAndPassword(entityBareJid.getLocalpart(), plainTextPassword) 55 | .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled) 56 | .setResource(entityBareJid.getResourceOrEmpty()) 57 | .setSendPresence(true) 58 | .build(); 59 | 60 | connection = new XMPPTCPConnection(config); 61 | connection.connect(); 62 | } catch (SmackException | IOException | XMPPException | InterruptedException e) { 63 | log.info("Could not connect to XMPP server.", e); 64 | return Optional.empty(); 65 | } 66 | return Optional.of(connection); 67 | } 68 | 69 | public void createAccount(XMPPTCPConnection connection, String username, String plainTextPassword) { 70 | AccountManager accountManager = AccountManager.getInstance(connection); 71 | accountManager.sensitiveOperationOverInsecureConnection(true); 72 | try { 73 | accountManager.createAccount(Localpart.from(username), plainTextPassword); 74 | } catch (SmackException.NoResponseException | 75 | XMPPException.XMPPErrorException | 76 | SmackException.NotConnectedException | 77 | InterruptedException | 78 | XmppStringprepException e) { 79 | throw new XMPPGenericException(username, e); 80 | } 81 | 82 | accountService.saveAccount(new Account(username, BCryptUtils.hash(plainTextPassword))); 83 | log.info("Account for user '{}' created.", username); 84 | } 85 | 86 | public void login(XMPPTCPConnection connection) { 87 | try { 88 | connection.login(); 89 | } catch (XMPPException | SmackException | IOException | InterruptedException e) { 90 | log.error("Login to XMPP server with user {} failed.", connection.getUser(), e); 91 | 92 | EntityFullJid user = connection.getUser(); 93 | throw new XMPPGenericException(user == null ? "unknown" : user.toString(), e); 94 | } 95 | log.info("User '{}' logged in.", connection.getUser()); 96 | } 97 | 98 | public void addIncomingMessageListener(XMPPTCPConnection connection, Session webSocketSession) { 99 | ChatManager chatManager = ChatManager.getInstanceFor(connection); 100 | chatManager.addIncomingListener((from, message, chat) -> xmppMessageTransmitter 101 | .sendResponse(message, webSocketSession)); 102 | log.info("Incoming message listener for user '{}' added.", connection.getUser()); 103 | } 104 | 105 | public void sendMessage(XMPPTCPConnection connection, String message, String to) { 106 | ChatManager chatManager = ChatManager.getInstanceFor(connection); 107 | try { 108 | Chat chat = chatManager.chatWith(JidCreate.entityBareFrom(to + "@" + xmppProperties.getDomain())); 109 | chat.send(message); 110 | log.info("Message sent to user '{}' from user '{}'.", to, connection.getUser()); 111 | } catch (XmppStringprepException | SmackException.NotConnectedException | InterruptedException e) { 112 | throw new XMPPGenericException(connection.getUser().toString(), e); 113 | } 114 | } 115 | 116 | public void addContact(XMPPTCPConnection connection, String to) { 117 | Roster roster = Roster.getInstanceFor(connection); 118 | 119 | if (!roster.isLoaded()) { 120 | try { 121 | roster.reloadAndWait(); 122 | } catch (SmackException.NotLoggedInException | SmackException.NotConnectedException | InterruptedException e) { 123 | log.error("XMPP error. Disconnecting and removing session...", e); 124 | throw new XMPPGenericException(connection.getUser().toString(), e); 125 | } 126 | } 127 | 128 | try { 129 | BareJid contact = JidCreate.bareFrom(to + "@" + xmppProperties.getDomain()); 130 | roster.createItemAndRequestSubscription(contact, to, null); 131 | log.info("Contact '{}' added to user '{}'.", to, connection.getUser()); 132 | } catch (XmppStringprepException | XMPPException.XMPPErrorException 133 | | SmackException.NotConnectedException | SmackException.NoResponseException 134 | | SmackException.NotLoggedInException | InterruptedException e) { 135 | log.error("XMPP error. Disconnecting and removing session...", e); 136 | throw new XMPPGenericException(connection.getUser().toString(), e); 137 | } 138 | } 139 | 140 | public void remove(XMPPTCPConnection connection, String to) { 141 | Roster roster = Roster.getInstanceFor(connection); 142 | 143 | if (!roster.isLoaded()) { 144 | try { 145 | roster.reloadAndWait(); 146 | } catch (SmackException.NotLoggedInException | SmackException.NotConnectedException | InterruptedException e) { 147 | log.error("XMPP error. Disconnecting and removing session...", e); 148 | throw new XMPPGenericException(connection.getUser().toString(), e); 149 | } 150 | } 151 | 152 | try { 153 | BareJid contact = JidCreate.bareFrom(to + "@" + xmppProperties.getDomain()); 154 | roster.removeEntry(roster.getEntry(contact)); 155 | log.info("User '{}' removed contact '{}'.", connection.getUser(), to); 156 | } catch (XmppStringprepException | XMPPException.XMPPErrorException 157 | | SmackException.NotConnectedException | SmackException.NoResponseException 158 | | SmackException.NotLoggedInException | InterruptedException e) { 159 | log.error("XMPP error. Disconnecting and removing session...", e); 160 | throw new XMPPGenericException(connection.getUser().toString(), e); 161 | } 162 | } 163 | 164 | public Set getContacts(XMPPTCPConnection connection) { 165 | Roster roster = Roster.getInstanceFor(connection); 166 | 167 | if (!roster.isLoaded()) { 168 | try { 169 | roster.reloadAndWait(); 170 | } catch (SmackException.NotLoggedInException | SmackException.NotConnectedException 171 | | InterruptedException e) { 172 | log.error("XMPP error. Disconnecting and removing session...", e); 173 | throw new XMPPGenericException(connection.getUser().toString(), e); 174 | } 175 | } 176 | 177 | return roster.getEntries(); 178 | } 179 | 180 | public void disconnect(XMPPTCPConnection connection) { 181 | Presence presence = PresenceBuilder.buildPresence() 182 | .ofType(Presence.Type.unavailable) 183 | .build(); 184 | try { 185 | connection.sendStanza(presence); 186 | } catch (SmackException.NotConnectedException | InterruptedException e) { 187 | log.error("XMPP error.", e); 188 | 189 | } 190 | connection.disconnect(); 191 | log.info("Connection closed for user '{}'.", connection.getUser()); 192 | } 193 | 194 | public void sendStanza(XMPPTCPConnection connection, Presence.Type type) { 195 | Presence presence = PresenceBuilder.buildPresence() 196 | .ofType(type) 197 | .build(); 198 | try { 199 | connection.sendStanza(presence); 200 | log.info("Status {} sent for user '{}'.", type, connection.getUser()); 201 | } catch (SmackException.NotConnectedException | InterruptedException e) { 202 | log.error("XMPP error.", e); 203 | throw new XMPPGenericException(connection.getUser().toString(), e); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /spring-xmpp-websocket-server/src/test/java/com/sergiomartinrubio/springxmppwebsocketsecurity/service/XMPPFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.sergiomartinrubio.springxmppwebsocketsecurity.service; 2 | 3 | import com.sergiomartinrubio.springxmppwebsocketsecurity.exception.XMPPGenericException; 4 | import com.sergiomartinrubio.springxmppwebsocketsecurity.facade.XMPPFacade; 5 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.Account; 6 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType; 7 | import com.sergiomartinrubio.springxmppwebsocketsecurity.model.WebsocketMessage; 8 | import com.sergiomartinrubio.springxmppwebsocketsecurity.websocket.utils.WebSocketTextMessageHelper; 9 | import com.sergiomartinrubio.springxmppwebsocketsecurity.xmpp.XMPPClient; 10 | import jakarta.websocket.Session; 11 | import org.jivesoftware.smack.packet.Presence; 12 | import org.jivesoftware.smack.tcp.XMPPTCPConnection; 13 | import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.jxmpp.stringprep.XmppStringprepException; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | import org.springframework.security.crypto.bcrypt.BCrypt; 21 | 22 | import java.util.Optional; 23 | 24 | import static com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType.ERROR; 25 | import static com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType.FORBIDDEN; 26 | import static com.sergiomartinrubio.springxmppwebsocketsecurity.model.MessageType.JOIN_SUCCESS; 27 | import static org.mockito.BDDMockito.given; 28 | import static org.mockito.BDDMockito.then; 29 | import static org.mockito.BDDMockito.willThrow; 30 | 31 | @ExtendWith(MockitoExtension.class) 32 | class XMPPFacadeTest { 33 | 34 | private static final String USERNAME = "user"; 35 | private static final String PASSWORD = "password"; 36 | private static final String MESSAGE = "hello world"; 37 | private static final String TO = "other-user"; 38 | 39 | @Mock 40 | private Session session; 41 | 42 | @Mock 43 | private AccountService accountService; 44 | 45 | @Mock 46 | private WebSocketTextMessageHelper webSocketTextMessageHelper; 47 | 48 | @Mock 49 | private XMPPClient xmppClient; 50 | 51 | @InjectMocks 52 | private XMPPFacade xmppFacade; 53 | 54 | 55 | @Test 56 | void startSessionShouldStartSessionWithoutCreatingAccountWhenAccountExistAndCorrectPassword() throws XmppStringprepException { 57 | // GIVEN 58 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 59 | .setXmppDomain("domain") 60 | .build(); 61 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 62 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 63 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 64 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 65 | 66 | // WHEN 67 | xmppFacade.startSession(session, USERNAME, PASSWORD); 68 | 69 | // THEN 70 | then(xmppClient).should().login(connection); 71 | then(xmppClient).should().addIncomingMessageListener(connection, session); 72 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(JOIN_SUCCESS, USERNAME)); 73 | then(xmppClient).shouldHaveNoMoreInteractions(); 74 | } 75 | 76 | @Test 77 | void startSessionShouldStartSessionAndCreateAccountWhenAccountDoesNotExist() throws XmppStringprepException { 78 | // GIVEN 79 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 80 | .setXmppDomain("domain") 81 | .build(); 82 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 83 | given(accountService.getAccount(USERNAME)).willReturn(Optional.empty()); 84 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 85 | 86 | // WHEN 87 | xmppFacade.startSession(session, USERNAME, PASSWORD); 88 | 89 | // THEN 90 | then(xmppClient).should().login(connection); 91 | then(xmppClient).should().addIncomingMessageListener(connection, session); 92 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(JOIN_SUCCESS, USERNAME)); 93 | then(xmppClient).should().createAccount(connection, USERNAME, PASSWORD); 94 | } 95 | 96 | @Test 97 | void startSessionShouldSendForbiddenMessageWhenWrongPassword() { 98 | // GIVEN 99 | String hashedPassword = BCrypt.hashpw("WRONG", BCrypt.gensalt()); 100 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 101 | 102 | // WHEN 103 | xmppFacade.startSession(session, USERNAME, PASSWORD); 104 | 105 | // THEN 106 | then(xmppClient).shouldHaveNoInteractions(); 107 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(FORBIDDEN, null)); 108 | } 109 | 110 | @Test 111 | void startSessionShouldSendErrorMessageWhenConnectionIsNotPresent() { 112 | // GIVEN 113 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 114 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 115 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.empty()); 116 | 117 | // WHEN 118 | xmppFacade.startSession(session, USERNAME, PASSWORD); 119 | 120 | // THEN 121 | then(xmppClient).shouldHaveNoMoreInteractions(); 122 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(ERROR, null)); 123 | } 124 | 125 | @Test 126 | void startSessionShouldSendErrorMessageWhenLoginThrowsXMPPGenericException() throws XmppStringprepException { 127 | // GIVEN 128 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 129 | .setXmppDomain("domain") 130 | .build(); 131 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 132 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 133 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 134 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 135 | willThrow(XMPPGenericException.class).given(xmppClient).login(connection); 136 | 137 | // WHEN 138 | xmppFacade.startSession(session, USERNAME, PASSWORD); 139 | 140 | // THEN 141 | then(xmppClient).should().disconnect(connection); 142 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(ERROR, null)); 143 | then(xmppClient).shouldHaveNoMoreInteractions(); 144 | } 145 | 146 | @Test 147 | void sendMessageShouldSendMessage() throws XmppStringprepException { 148 | // GIVEN 149 | WebsocketMessage message = WebsocketMessage.builder() 150 | .content(MESSAGE) 151 | .to(TO) 152 | .messageType(MessageType.NEW_MESSAGE) 153 | .build(); 154 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 155 | .setXmppDomain("domain") 156 | .build(); 157 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 158 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 159 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 160 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 161 | xmppFacade.startSession(session, USERNAME, PASSWORD); 162 | 163 | // WHEN 164 | xmppFacade.sendMessage(message, session); 165 | 166 | // THEN 167 | then(xmppClient).should().sendMessage(connection, MESSAGE, TO); 168 | } 169 | 170 | @Test 171 | void sendMessageShouldSendErrorMessageWhenXMPPGenericException() throws XmppStringprepException { 172 | // GIVEN 173 | WebsocketMessage message = WebsocketMessage.builder() 174 | .content(MESSAGE) 175 | .to(TO) 176 | .messageType(MessageType.NEW_MESSAGE) 177 | .build(); 178 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 179 | .setXmppDomain("domain") 180 | .build(); 181 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 182 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 183 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 184 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 185 | xmppFacade.startSession(session, USERNAME, PASSWORD); 186 | willThrow(XMPPGenericException.class).given(xmppClient).sendMessage(connection, MESSAGE, TO); 187 | 188 | // WHEN 189 | xmppFacade.sendMessage(message, session); 190 | 191 | // THEN 192 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(ERROR, null)); 193 | } 194 | 195 | @Test 196 | void sendMessageShouldDoNothingWhenNotFoundConnection() { 197 | // GIVEN 198 | WebsocketMessage message = WebsocketMessage.builder() 199 | .content(MESSAGE) 200 | .to(TO) 201 | .messageType(MessageType.NEW_MESSAGE) 202 | .build(); 203 | 204 | // WHEN 205 | xmppFacade.sendMessage(message, session); 206 | 207 | // THEN 208 | then(xmppClient).shouldHaveNoInteractions(); 209 | } 210 | 211 | @Test 212 | void disconnectShouldSendStanzaAndDisconnect() throws XmppStringprepException { 213 | // GIVEN 214 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 215 | .setXmppDomain("domain") 216 | .build(); 217 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 218 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 219 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 220 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 221 | xmppFacade.startSession(session, USERNAME, PASSWORD); 222 | 223 | // WHEN 224 | xmppFacade.disconnect(session); 225 | 226 | // THEN 227 | then(xmppClient).should().sendStanza(connection, Presence.Type.unavailable); 228 | then(xmppClient).should().disconnect(connection); 229 | } 230 | 231 | @Test 232 | void disconnectShouldSendErrorMessageWhenXMPPGenericException() throws XmppStringprepException { 233 | // GIVEN 234 | XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() 235 | .setXmppDomain("domain") 236 | .build(); 237 | XMPPTCPConnection connection = new XMPPTCPConnection(configuration); 238 | String hashedPassword = BCrypt.hashpw(PASSWORD, BCrypt.gensalt()); 239 | given(accountService.getAccount(USERNAME)).willReturn(Optional.of(new Account(USERNAME, hashedPassword))); 240 | given(xmppClient.connect(USERNAME, PASSWORD)).willReturn(Optional.of(connection)); 241 | xmppFacade.startSession(session, USERNAME, PASSWORD); 242 | willThrow(XMPPGenericException.class).given(xmppClient).sendStanza(connection, Presence.Type.unavailable); 243 | 244 | 245 | // WHEN 246 | xmppFacade.disconnect(session); 247 | 248 | // THEN 249 | then(webSocketTextMessageHelper).should().send(session, createTextMessage(ERROR, null)); 250 | } 251 | 252 | @Test 253 | void disconnectShouldDoNothingWhenNotFoundConnection() { 254 | // WHEN 255 | xmppFacade.disconnect(session); 256 | 257 | // THEN 258 | then(xmppClient).shouldHaveNoInteractions(); 259 | } 260 | 261 | private WebsocketMessage createTextMessage(MessageType type, String to) { 262 | return WebsocketMessage.builder() 263 | .to(to) 264 | .messageType(type) 265 | .build(); 266 | } 267 | } 268 | --------------------------------------------------------------------------------