From 35c13d83bdf842c848bae8f12d9ec60fa6dbedac Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 12 May 2022 22:10:42 +0200 Subject: [PATCH] Added stuff. --- pom.xml | 4 + railsignal-app/.env.development | 3 +- railsignal-app/.env.production | 3 +- railsignal-app/src/components/AppNavbar.vue | 6 -- railsignal-app/src/main.js | 10 +++ railsignal-app/src/stores/railSystemsStore.js | 27 ++++++- .../railsignalapi/live/ComponentDownlink.java | 24 ++++++ .../live/ComponentDownlinkService.java | 34 ++++++++ .../live/tcp_socket/TcpDownlink.java | 24 ++++++ .../live/tcp_socket/TcpSocketServer.java | 79 +++++++++++++++++++ .../live/websocket/AppWebsocketHandler.java | 35 ++++++++ .../AppWebsocketHandshakeInterceptor.java} | 27 ++++--- .../websocket/ComponentWebsocketHandler.java | 49 ++++++++++++ ...omponentWebsocketHandshakeInterceptor.java | 56 +++++++++++++ .../live/websocket/WebsocketConfig.java | 40 ++++++++++ .../live/websocket/WebsocketDownlink.java | 20 +++++ .../model/ComponentAccessToken.java | 4 +- .../andrewl/railsignalapi/util/JsonUtils.java | 12 +++ .../websocket/ComponentWebSocketConfig.java | 21 ----- .../websocket/SignalWebSocketHandler.java | 47 ----------- 20 files changed, 435 insertions(+), 90 deletions(-) create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java rename src/main/java/nl/andrewl/railsignalapi/{websocket/ComponentWebSocketHandshakeInterceptor.java => live/websocket/AppWebsocketHandshakeInterceptor.java} (53%) create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java delete mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java delete mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java diff --git a/pom.xml b/pom.xml index 93c7516..af347ba 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-thymeleaf diff --git a/railsignal-app/.env.development b/railsignal-app/.env.development index 430da52..a90eb52 100644 --- a/railsignal-app/.env.development +++ b/railsignal-app/.env.development @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:8080/api \ No newline at end of file +VITE_API_URL=http://localhost:8080/api +VITE_WS_URL=ws://localhost:8080/api/ws/app \ No newline at end of file diff --git a/railsignal-app/.env.production b/railsignal-app/.env.production index 430da52..a90eb52 100644 --- a/railsignal-app/.env.production +++ b/railsignal-app/.env.production @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:8080/api \ No newline at end of file +VITE_API_URL=http://localhost:8080/api +VITE_WS_URL=ws://localhost:8080/api/ws/app \ No newline at end of file diff --git a/railsignal-app/src/components/AppNavbar.vue b/railsignal-app/src/components/AppNavbar.vue index 62bcef5..cf86a87 100644 --- a/railsignal-app/src/components/AppNavbar.vue +++ b/railsignal-app/src/components/AppNavbar.vue @@ -63,12 +63,6 @@ export default { components: {AddRailSystem: AddRailSystemModal, ConfirmModal}, setup() { const rsStore = useRailSystemsStore(); - rsStore.$subscribe(mutation => { - const evt = mutation.events; - if (evt.key === "selectedRailSystem" && evt.newValue !== null) { - rsStore.fetchSelectedRailSystemData(); - } - }); return { rsStore }; diff --git a/railsignal-app/src/main.js b/railsignal-app/src/main.js index 62748cd..c0af016 100644 --- a/railsignal-app/src/main.js +++ b/railsignal-app/src/main.js @@ -3,10 +3,20 @@ import { createPinia } from 'pinia'; import App from './App.vue' import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap"; +import {useRailSystemsStore} from "./stores/railSystemsStore"; const pinia = createPinia(); const app = createApp(App); app.use(pinia); +// Configure rail system updates. +const rsStore = useRailSystemsStore(); +rsStore.$subscribe(mutation => { + const evt = mutation.events; + if (evt.key === "selectedRailSystem" && evt.newValue !== null) { + rsStore.onSelectedRailSystemChanged(); + } +}); + app.mount('#app') diff --git a/railsignal-app/src/stores/railSystemsStore.js b/railsignal-app/src/stores/railSystemsStore.js index b2d506f..5d893c0 100644 --- a/railsignal-app/src/stores/railSystemsStore.js +++ b/railsignal-app/src/stores/railSystemsStore.js @@ -5,10 +5,17 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', { state: () => ({ railSystems: [], /** - * @type {{segments: [Object], components: [Object], selectedComponent: Object} | null} + * @type {{ + * segments: [Object], + * components: [Object], + * selectedComponent: Object | null, + * websocket: WebSocket | null + * } | null} */ selectedRailSystem: null, - apiUrl: import.meta.env.VITE_API_URL + websocket: null, + apiUrl: import.meta.env.VITE_API_URL, + wsUrl: import.meta.env.VITE_WS_URL }), actions: { refreshRailSystems() { @@ -65,10 +72,24 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', { }); }); }, - fetchSelectedRailSystemData() { + onSelectedRailSystemChanged() { if (!this.selectedRailSystem) return; this.refreshSegments(this.selectedRailSystem); this.refreshAllComponents(this.selectedRailSystem); + if (this.websocket !== null) { + this.websocket.close(); + } + console.log(this.wsUrl); + this.websocket = new WebSocket(this.wsUrl); + this.websocket.onopen = event => { + console.log("Opened websocket connection."); + }; + this.websocket.onclose = event => { + console.log("Closed websocket connection."); + }; + this.websocket.onmessage = () => { + + }; }, addSegment(name) { const rs = this.selectedRailSystem; diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java new file mode 100644 index 0000000..67bb723 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java @@ -0,0 +1,24 @@ +package nl.andrewl.railsignalapi.live; + +import lombok.Getter; + +public abstract class ComponentDownlink { + @Getter + private final long id; + + public ComponentDownlink(long id) { + this.id = id; + } + + public abstract void send(Object msg) throws Exception; + + @Override + public boolean equals(Object o) { + return o instanceof ComponentDownlink cd && cd.id == this.id; + } + + @Override + public int hashCode() { + return Long.hashCode(id); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java new file mode 100644 index 0000000..e2f3147 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java @@ -0,0 +1,34 @@ +package nl.andrewl.railsignalapi.live; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A service that manages all the active component downlink connections. + */ +@Service +@RequiredArgsConstructor +public class ComponentDownlinkService { + private final Map> componentDownlinks = new HashMap<>(); + + public synchronized void registerDownlink(ComponentDownlink downlink, Set componentIds) { + componentDownlinks.put(downlink, componentIds); + } + + public synchronized void deregisterDownlink(ComponentDownlink downlink) { + componentDownlinks.remove(downlink); + } + + public synchronized void deregisterDownlink(long tokenId) { + List removeSet = componentDownlinks.keySet().stream() + .filter(downlink -> downlink.getId() == tokenId).toList(); + for (var downlink : removeSet) { + componentDownlinks.remove(downlink); + } + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java new file mode 100644 index 0000000..007c76e --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java @@ -0,0 +1,24 @@ +package nl.andrewl.railsignalapi.live.tcp_socket; + +import nl.andrewl.railsignalapi.live.ComponentDownlink; +import nl.andrewl.railsignalapi.util.JsonUtils; + +import java.io.DataOutputStream; +import java.nio.charset.StandardCharsets; + +public class TcpDownlink extends ComponentDownlink { + private final DataOutputStream out; + + public TcpDownlink(long id, DataOutputStream out) { + super(id); + this.out = out; + } + + @Override + public void send(Object msg) throws Exception { + byte[] jsonBytes = JsonUtils.toJson(msg).getBytes(StandardCharsets.UTF_8); + out.writeInt(jsonBytes.length); + out.write(jsonBytes); + out.flush(); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java new file mode 100644 index 0000000..69b298e --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java @@ -0,0 +1,79 @@ +package nl.andrewl.railsignalapi.live.tcp_socket; + +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository; +import nl.andrewl.railsignalapi.model.ComponentAccessToken; +import nl.andrewl.railsignalapi.util.JsonUtils; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * A plain TCP server socket which can be used to connect to components that + * don't have access to a full websocket client implementation. + */ +@Component +@Slf4j +public class TcpSocketServer { + private final ServerSocket serverSocket; + private final ComponentAccessTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public TcpSocketServer(ComponentAccessTokenRepository tokenRepository, PasswordEncoder passwordEncoder) throws IOException { + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + this.serverSocket = new ServerSocket(); + serverSocket.setReuseAddress(true); + serverSocket.bind(new InetSocketAddress("localhost", 8081)); + } + + @EventListener(ApplicationReadyEvent.class) + public void runServer() { + new Thread(() -> { + log.info("Starting TCP Socket for Component links at " + serverSocket.getInetAddress()); + while (!serverSocket.isClosed()) { + try { + Socket socket = serverSocket.accept(); + initializeConnection(socket); + } catch (IOException e) { + log.warn("An IOException occurred while waiting to accept a TCP socket connection.", e); + } + } + }); + } + + @EventListener(ContextClosedEvent.class) + public void closeServer() throws IOException { + serverSocket.close(); + } + + private void initializeConnection(Socket socket) throws IOException { + DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + DataInputStream in = new DataInputStream(socket.getInputStream()); + int tokenLength = in.readInt(); + String rawToken = new String(in.readNBytes(tokenLength)); + if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) { + byte[] respBytes = JsonUtils.toJson(Map.of("message", "Invalid token")).getBytes(StandardCharsets.UTF_8); + out.writeInt(respBytes.length); + out.write(respBytes); + socket.close(); + } + Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE)); + for (var token : tokens) { + if (passwordEncoder.matches(rawToken, token.getTokenHash())) { + + } + } + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java new file mode 100644 index 0000000..5599f1f --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java @@ -0,0 +1,35 @@ +package nl.andrewl.railsignalapi.live.websocket; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * A websocket handler for all websocket connections to the Rail Signal web + * app frontend. + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class AppWebsocketHandler extends TextWebSocketHandler { + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + super.afterConnectionEstablished(session); + log.info("App websocket session established."); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + super.handleTextMessage(session, message); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + super.afterConnectionClosed(session, status); + log.info("App websocket session closed."); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java similarity index 53% rename from src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java rename to src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java index e25c88b..7c75921 100644 --- a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java @@ -1,32 +1,39 @@ -package nl.andrewl.railsignalapi.websocket; +package nl.andrewl.railsignalapi.live.websocket; import lombok.RequiredArgsConstructor; -import nl.andrewl.railsignalapi.dao.ComponentRepository; import nl.andrewl.railsignalapi.dao.RailSystemRepository; +import org.springframework.http.HttpStatus; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; -import java.util.Arrays; import java.util.Map; +/** + * This interceptor is used to check incoming websocket connections from the + * web app, to ensure that they're directed towards a valid rail system. + */ @Component @RequiredArgsConstructor -public class ComponentWebSocketHandshakeInterceptor implements HandshakeInterceptor { +public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor { private final RailSystemRepository railSystemRepository; - private final ComponentRepository componentRepository; @Override - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { - String[] queryParams = request.getURI().getQuery().split("&"); - System.out.println(Arrays.toString(queryParams)); - return false; + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { + String path = request.getURI().getPath(); + Long railSystemId = Long.parseLong(path.substring(path.lastIndexOf('/'))); + if (!railSystemRepository.existsById(railSystemId)) { + response.setStatusCode(HttpStatus.NOT_FOUND); + return false; + } + attributes.put("railSystemId", railSystemId); + return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { - + // Nothing to do after the handshake. } } diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java new file mode 100644 index 0000000..f39e940 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java @@ -0,0 +1,49 @@ +package nl.andrewl.railsignalapi.live.websocket; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository; +import nl.andrewl.railsignalapi.live.ComponentDownlinkService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Handler for websocket connections that components open to send and receive + * real-time updates from the server. + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class ComponentWebsocketHandler extends TextWebSocketHandler { + private final ComponentAccessTokenRepository tokenRepository; + private final ComponentDownlinkService componentDownlinkService; + + @Override + @Transactional(readOnly = true) + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + long tokenId = (long) session.getAttributes().get("tokenId"); + var token = tokenRepository.findById(tokenId).orElseThrow(); + Set componentIds = token.getComponents().stream() + .map(nl.andrewl.railsignalapi.model.component.Component::getId) + .collect(Collectors.toSet()); + componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session), componentIds); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { +// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class); + //signalService.handleSignalUpdate(msg); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + componentDownlinkService.deregisterDownlink((long) session.getAttributes().get("tokenId")); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java new file mode 100644 index 0000000..189eeba --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java @@ -0,0 +1,56 @@ +package nl.andrewl.railsignalapi.live.websocket; + +import lombok.RequiredArgsConstructor; +import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository; +import nl.andrewl.railsignalapi.model.ComponentAccessToken; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +/** + * An interceptor that checks incoming component websocket connections to + * ensure that they have a required "token" query parameter that refers to one + * or more components in a rail system. If the token is valid, we pass its id + * on as an attribute for the handler that will register the connection. + */ +@Component +@RequiredArgsConstructor +public class ComponentWebsocketHandshakeInterceptor implements HandshakeInterceptor { + private final ComponentAccessTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { + String query = request.getURI().getQuery(); + int tokenIdx = query.lastIndexOf("token="); + if (tokenIdx == -1) { + response.setStatusCode(HttpStatus.BAD_REQUEST); + return false; + } + String rawToken = query.substring(tokenIdx); + if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) { + response.setStatusCode(HttpStatus.BAD_REQUEST); + return false; + } + Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE)); + for (var token : tokens) { + if (passwordEncoder.matches(rawToken, token.getTokenHash())) { + attributes.put("tokenId", token.getId()); + return true; + } + } + response.setStatusCode(HttpStatus.UNAUTHORIZED); + return false; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + // Don't need to do anything after the handshake. + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java new file mode 100644 index 0000000..00bd502 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java @@ -0,0 +1,40 @@ +package nl.andrewl.railsignalapi.live.websocket; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +import java.util.Set; + +/** + * Configuration for Rail Signal's websockets. This includes both app and + * component connections. + */ +@Configuration +@EnableWebSocket +@RequiredArgsConstructor +public class WebsocketConfig implements WebSocketConfigurer { + private final ComponentWebsocketHandler componentHandler; + private final ComponentWebsocketHandshakeInterceptor componentInterceptor; + private final AppWebsocketHandler appHandler; + private final AppWebsocketHandshakeInterceptor appInterceptor; + private final Environment env; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(componentHandler, "/api/ws/component") + .setAllowedOrigins("*") + .addInterceptors(componentInterceptor); + WebSocketHandlerRegistration appHandlerReg = registry.addHandler(appHandler, "/api/ws/app/*") + .addInterceptors(appInterceptor); + // If we're in a development profile, allow any origin to access the app websocket. + // This is so that we can use a standalone JS dev server. + if (Set.of(env.getActiveProfiles()).contains("development")) { + appHandlerReg.setAllowedOrigins("*"); + } + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java new file mode 100644 index 0000000..a76e635 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java @@ -0,0 +1,20 @@ +package nl.andrewl.railsignalapi.live.websocket; + +import nl.andrewl.railsignalapi.live.ComponentDownlink; +import nl.andrewl.railsignalapi.util.JsonUtils; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +public class WebsocketDownlink extends ComponentDownlink { + private final WebSocketSession webSocketSession; + + public WebsocketDownlink(long id, WebSocketSession webSocketSession) { + super(id); + this.webSocketSession = webSocketSession; + } + + @Override + public void send(Object msg) throws Exception { + webSocketSession.sendMessage(new TextMessage(JsonUtils.toJson(msg))); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java index 61cd0d6..72ee36e 100644 --- a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java +++ b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java @@ -18,6 +18,8 @@ import java.util.Set; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class ComponentAccessToken { + public static final byte PREFIX_SIZE = 7; + @Id @GeneratedValue private Long id; @@ -37,7 +39,7 @@ public class ComponentAccessToken { /** * A short prefix of the token, which is useful for speeding up lookup. */ - @Column(nullable = false, length = 7) + @Column(nullable = false, length = PREFIX_SIZE) private String tokenPrefix; /** diff --git a/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java new file mode 100644 index 0000000..1c13973 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java @@ -0,0 +1,12 @@ +package nl.andrewl.railsignalapi.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonUtils { + private static final ObjectMapper mapper = new ObjectMapper(); + + public static String toJson(Object o) throws JsonProcessingException { + return mapper.writeValueAsString(o); + } +} diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java deleted file mode 100644 index 2881be3..0000000 --- a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package nl.andrewl.railsignalapi.websocket; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - -@Configuration -@EnableWebSocket -@RequiredArgsConstructor -public class ComponentWebSocketConfig implements WebSocketConfigurer { - private final SignalWebSocketHandler webSocketHandler; - private final ComponentWebSocketHandshakeInterceptor handshakeInterceptor; - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(webSocketHandler, "/api/ws/component") - .addInterceptors(handshakeInterceptor); - } -} diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java deleted file mode 100644 index 20e9b9d..0000000 --- a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package nl.andrewl.railsignalapi.websocket; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import java.util.HashSet; -import java.util.Set; - -@Component -@RequiredArgsConstructor -@Slf4j -public class SignalWebSocketHandler extends TextWebSocketHandler { - private final ObjectMapper mapper = new ObjectMapper(); - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - String signalIdHeader = session.getHandshakeHeaders().getFirst("X-RailSignal-SignalId"); - if (signalIdHeader == null || signalIdHeader.isBlank()) { - session.close(CloseStatus.PROTOCOL_ERROR); - return; - } - Set ids = new HashSet<>(); - for (var idStr : signalIdHeader.split(",")) { - ids.add(Long.parseLong(idStr.trim())); - } - //signalService.registerSignalWebSocketSession(ids, session); - log.info("Connection established with signals {}.", ids); - } - - @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { -// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class); - //signalService.handleSignalUpdate(msg); - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { - //signalService.deregisterSignalWebSocketSession(session); - log.info("Closed connection {}. Status: {}", session.getId(), status.toString()); - } -}