Added stuff.
This commit is contained in:
		
							parent
							
								
									ecd9549e77
								
							
						
					
					
						commit
						35c13d83bd
					
				
							
								
								
									
										4
									
								
								pom.xml
								
								
								
								
							
							
						
						
									
										4
									
								
								pom.xml
								
								
								
								
							|  | @ -25,6 +25,10 @@ | |||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-web</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-security</artifactId> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.springframework.boot</groupId> | ||||
| 			<artifactId>spring-boot-starter-thymeleaf</artifactId> | ||||
|  |  | |||
|  | @ -1 +1,2 @@ | |||
| VITE_API_URL=http://localhost:8080/api | ||||
| VITE_API_URL=http://localhost:8080/api | ||||
| VITE_WS_URL=ws://localhost:8080/api/ws/app | ||||
|  | @ -1 +1,2 @@ | |||
| VITE_API_URL=http://localhost:8080/api | ||||
| VITE_API_URL=http://localhost:8080/api | ||||
| VITE_WS_URL=ws://localhost:8080/api/ws/app | ||||
|  | @ -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 | ||||
|     }; | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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<ComponentDownlink, Set<Long>> componentDownlinks = new HashMap<>(); | ||||
| 
 | ||||
| 	public synchronized void registerDownlink(ComponentDownlink downlink, Set<Long> componentIds) { | ||||
| 		componentDownlinks.put(downlink, componentIds); | ||||
| 	} | ||||
| 
 | ||||
| 	public synchronized void deregisterDownlink(ComponentDownlink downlink) { | ||||
| 		componentDownlinks.remove(downlink); | ||||
| 	} | ||||
| 
 | ||||
| 	public synchronized void deregisterDownlink(long tokenId) { | ||||
| 		List<ComponentDownlink> removeSet = componentDownlinks.keySet().stream() | ||||
| 				.filter(downlink -> downlink.getId() == tokenId).toList(); | ||||
| 		for (var downlink : removeSet) { | ||||
| 			componentDownlinks.remove(downlink); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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(); | ||||
| 	} | ||||
| } | ||||
|  | @ -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<ComponentAccessToken> tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE)); | ||||
| 		for (var token : tokens) { | ||||
| 			if (passwordEncoder.matches(rawToken, token.getTokenHash())) { | ||||
| 
 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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."); | ||||
| 	} | ||||
| } | ||||
|  | @ -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<nl.andrewl.railsignalapi.model.component.Component> componentRepository; | ||||
| 
 | ||||
| 	@Override | ||||
| 	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> 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<String, Object> 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. | ||||
| 	} | ||||
| } | ||||
|  | @ -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<Long> 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")); | ||||
| 	} | ||||
| } | ||||
|  | @ -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<String, Object> 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<ComponentAccessToken> 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. | ||||
| 	} | ||||
| } | ||||
|  | @ -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("*"); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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))); | ||||
| 	} | ||||
| } | ||||
|  | @ -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; | ||||
| 
 | ||||
| 	/** | ||||
|  |  | |||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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<Long> 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()); | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue