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>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<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},
|
components: {AddRailSystem: AddRailSystemModal, ConfirmModal},
|
||||||
setup() {
|
setup() {
|
||||||
const rsStore = useRailSystemsStore();
|
const rsStore = useRailSystemsStore();
|
||||||
rsStore.$subscribe(mutation => {
|
|
||||||
const evt = mutation.events;
|
|
||||||
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
|
|
||||||
rsStore.fetchSelectedRailSystemData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
rsStore
|
rsStore
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,20 @@ import { createPinia } from 'pinia';
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
|
import {useRailSystemsStore} from "./stores/railSystemsStore";
|
||||||
|
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(pinia);
|
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')
|
app.mount('#app')
|
||||||
|
|
|
@ -5,10 +5,17 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
railSystems: [],
|
railSystems: [],
|
||||||
/**
|
/**
|
||||||
* @type {{segments: [Object], components: [Object], selectedComponent: Object} | null}
|
* @type {{
|
||||||
|
* segments: [Object],
|
||||||
|
* components: [Object],
|
||||||
|
* selectedComponent: Object | null,
|
||||||
|
* websocket: WebSocket | null
|
||||||
|
* } | null}
|
||||||
*/
|
*/
|
||||||
selectedRailSystem: 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: {
|
actions: {
|
||||||
refreshRailSystems() {
|
refreshRailSystems() {
|
||||||
|
@ -65,10 +72,24 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchSelectedRailSystemData() {
|
onSelectedRailSystemChanged() {
|
||||||
if (!this.selectedRailSystem) return;
|
if (!this.selectedRailSystem) return;
|
||||||
this.refreshSegments(this.selectedRailSystem);
|
this.refreshSegments(this.selectedRailSystem);
|
||||||
this.refreshAllComponents(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) {
|
addSegment(name) {
|
||||||
const rs = this.selectedRailSystem;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
|
||||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
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
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ComponentWebSocketHandshakeInterceptor implements HandshakeInterceptor {
|
public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
private final RailSystemRepository railSystemRepository;
|
private final RailSystemRepository railSystemRepository;
|
||||||
private final ComponentRepository<nl.andrewl.railsignalapi.model.component.Component> componentRepository;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||||
String[] queryParams = request.getURI().getQuery().split("&");
|
String path = request.getURI().getPath();
|
||||||
System.out.println(Arrays.toString(queryParams));
|
Long railSystemId = Long.parseLong(path.substring(path.lastIndexOf('/')));
|
||||||
return false;
|
if (!railSystemRepository.existsById(railSystemId)) {
|
||||||
|
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
attributes.put("railSystemId", railSystemId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
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)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class ComponentAccessToken {
|
public class ComponentAccessToken {
|
||||||
|
public static final byte PREFIX_SIZE = 7;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private Long id;
|
||||||
|
@ -37,7 +39,7 @@ public class ComponentAccessToken {
|
||||||
/**
|
/**
|
||||||
* A short prefix of the token, which is useful for speeding up lookup.
|
* 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;
|
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