diff --git a/build.gradle b/build.gradle
index d23270b..5140ea7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'
+ implementation 'org.springframework.boot:spring-boot-starter-websocket'
+ implementation 'org.springframework:spring-websocket:5.2.4.RELEASE'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.springframework.security:spring-security-web'
implementation 'org.postgresql:postgresql'
diff --git a/gradle.yml b/gradle.yml
new file mode 100644
index 0000000..51fefa4
--- /dev/null
+++ b/gradle.yml
@@ -0,0 +1,39 @@
+# vim: set ts=2 sw=2 et tw=80:
+image: gradle:jdk13
+
+stages:
+ - build
+ - test
+ - deploy
+
+smarthut_build:
+ stage: build
+ script:
+ - gradle assemble
+ artifacts:
+ paths:
+ - build/libs/*.jar
+ expire_in: 1 week
+
+smarthut_test:
+ stage: test
+ script:
+ - gradle check
+
+smarthut_deploy:
+ stage: deploy
+ image: docker:latest
+ services:
+ - docker:dind
+ variables:
+ DOCKER_DRIVER: overlay
+ before_script:
+ - docker version
+ - docker info
+ - docker login -u smarthutsm -p $CI_DOCKER_PASS
+ script:
+ - "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} --pull ."
+ - "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}"
+ after_script:
+ - docker logout
+
diff --git a/socket_test.html b/socket_test.html
new file mode 100644
index 0000000..90feba2
--- /dev/null
+++ b/socket_test.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
Waiting for authentication...
+
+
+
+
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java
index 853083b..e0cbb6a 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java
@@ -20,7 +20,7 @@ public class JWTRequestFilter extends OncePerRequestFilter {
@Autowired private JWTUserDetailsService jwtUserDetailsService;
- @Autowired private JWTTokenUtil jwtTokenUtil;
+ @Autowired private JWTTokenUtils jwtTokenUtils;
@Override
protected void doFilterInternal(
@@ -30,13 +30,11 @@ public class JWTRequestFilter extends OncePerRequestFilter {
String username = null;
String jwtToken = null;
- // JWT Token is in th
- // e form "Bearer token". Remove Bearer word and get
- // only the Token
+ // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
- username = jwtTokenUtil.getUsernameFromToken(jwtToken);
+ username = jwtTokenUtils.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
@@ -44,14 +42,15 @@ public class JWTRequestFilter extends OncePerRequestFilter {
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
- } // Once we get the token validate it.
+ }
+
+ // Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails =
this.jwtUserDetailsService.loadUserByUsername(
username); // if token is valid configure Spring Security to manually
- // set
- // authentication
- if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
+ // set authentication
+ if (jwtTokenUtils.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java
deleted file mode 100644
index 40d369f..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtil.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
-
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.stereotype.Component;
-
-@Component
-public class JWTTokenUtil {
- public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
-
- @Value("${jwt.secret}")
- private String secret;
-
- // retrieve username from jwt token
- public String getUsernameFromToken(String token) {
- return getClaimFromToken(token, Claims::getSubject);
- }
-
- // retrieve expiration date from jwt token
- public Date getExpirationDateFromToken(String token) {
- return getClaimFromToken(token, Claims::getExpiration);
- }
-
- public T getClaimFromToken(String token, Function claimsResolver) {
- final Claims claims = getAllClaimsFromToken(token);
- return claimsResolver.apply(claims);
- }
-
- // for retrieveing any information from token we will need the secret key
- private Claims getAllClaimsFromToken(String token) {
- return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
- } // check if the token has expired
-
- private Boolean isTokenExpired(String token) {
- final Date expiration = getExpirationDateFromToken(token);
- return expiration.before(new Date());
- } // generate token for user
-
- public String generateToken(UserDetails userDetails) {
- Map claims = new HashMap<>();
- return doGenerateToken(claims, userDetails.getUsername());
- }
-
- // while creating the token -
- // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
- // 2. Sign the JWT using the HS512 algorithm and secret key.
- // 3. According to JWS Compact
- // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
- // compaction of the JWT to a URL-safe string
- private String doGenerateToken(Map claims, String subject) {
- return Jwts.builder()
- .setClaims(claims)
- .setSubject(subject)
- .setIssuedAt(new Date(System.currentTimeMillis()))
- .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
- .signWith(SignatureAlgorithm.HS512, secret)
- .compact();
- }
-
- // validate token
- public Boolean validateToken(String token, UserDetails userDetails) {
- final String username = getUsernameFromToken(token);
- return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
- }
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java
new file mode 100644
index 0000000..f6943a8
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java
@@ -0,0 +1,84 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.function.Function;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+/** A utility class to handle JWTs */
+@Component
+public class JWTTokenUtils {
+ /** The duration in seconds of the validity of a single token */
+ private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
+
+ /** The secret key used to encrypt all JWTs */
+ @Value("${jwt.secret}")
+ private String secret;
+
+ /**
+ * Retrieves the claimed username from a given token
+ *
+ * @param token the token to inspect
+ * @return the username
+ */
+ public String getUsernameFromToken(String token) {
+ return getClaimFromToken(token, Claims::getSubject);
+ }
+
+ /**
+ * Returns whether the token given is expired or not
+ *
+ * @param token the given token
+ * @return true if expired, false if not
+ */
+ public Boolean isTokenExpired(String token) {
+ final Date expiration = getClaimFromToken(token, Claims::getExpiration);
+ return expiration.before(new Date());
+ }
+
+ /**
+ * Creates a new JWT for a given user. While creating the token - 1. Define claims of the token,
+ * like Issuer, Expiration, Subject, and the ID 2. Sign the JWT using the HS512 algorithm and
+ * secret key. 3. According to JWS Compact Serialization
+ * (https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) compaction of
+ * the JWT to a URL-safe string
+ *
+ * @param user the user to which create a JWT
+ * @return the newly generated token
+ */
+ public String generateToken(UserDetails user) {
+ return Jwts.builder()
+ .setClaims(new HashMap<>())
+ .setSubject(user.getUsername())
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
+ .signWith(SignatureAlgorithm.HS512, secret)
+ .compact();
+ }
+
+ /**
+ * Validates the token given against matching userDetails
+ *
+ * @param token the token given
+ * @param userDetails user details to validate against
+ * @return true if valid, false if not
+ */
+ public Boolean validateToken(String token, UserDetails userDetails) {
+ final String username = getUsernameFromToken(token);
+ return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+ }
+
+ private T getClaimFromToken(String token, Function claimsResolver) {
+ final Claims claims = getAllClaimsFromToken(token);
+ return claimsResolver.apply(claims);
+ }
+
+ private Claims getAllClaimsFromToken(String token) {
+ return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
index 253998d..ec116c3 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/WebSecurityConfig.java
@@ -51,6 +51,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// dont authenticate this particular request
.authorizeRequests()
.antMatchers(
+ "/sensor-socket",
"/auth/login",
"/swagger-ui.html",
"/register",
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
index 1a1e266..3160e1c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
@@ -1,6 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtil;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserUpdateRequest;
@@ -26,7 +26,7 @@ public class AuthenticationController {
private final UserRepository userRepository;
- private final JWTTokenUtil jwtTokenUtil;
+ private final JWTTokenUtils jwtTokenUtils;
private final JWTUserDetailsService userDetailsService;
@@ -35,11 +35,11 @@ public class AuthenticationController {
public AuthenticationController(
AuthenticationManager authenticationManager,
UserRepository userRepository,
- JWTTokenUtil jwtTokenUtil,
+ JWTTokenUtils jwtTokenUtils,
JWTUserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
- this.jwtTokenUtil = jwtTokenUtil;
+ this.jwtTokenUtils = jwtTokenUtils;
this.userDetailsService = userDetailsService;
}
@@ -68,7 +68,7 @@ public class AuthenticationController {
authenticationRequest.getUsernameOrEmail());
}
- final String token = jwtTokenUtil.generateToken(userDetails);
+ final String token = jwtTokenUtils.generateToken(userDetails);
return new JWTResponse(token);
}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java
new file mode 100644
index 0000000..e0b9249
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/AuthenticationMessageListener.java
@@ -0,0 +1,96 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.jsonwebtoken.ExpiredJwtException;
+import java.io.IOException;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/** Generates MessageHandlers for unauthenticated socket sessions */
+@Component
+public class AuthenticationMessageListener {
+
+ private Gson gson = new Gson();
+
+ private JWTTokenUtils jwtTokenUtils;
+
+ private UserRepository userRepository;
+
+ @Autowired
+ public AuthenticationMessageListener(
+ JWTTokenUtils jwtTokenUtils, UserRepository userRepository) {
+ this.jwtTokenUtils = jwtTokenUtils;
+ this.userRepository = userRepository;
+ }
+
+ /**
+ * Generates a new message handler to handle socket authentication
+ *
+ * @param session the session to which authentication must be checked
+ * @param authorizedSetter function to call once user is authenticated
+ * @return a new message handler to handle socket authentication
+ */
+ MessageHandler.Whole newHandler(
+ final Session session, BiConsumer authorizedSetter) {
+ return new MessageHandler.Whole<>() {
+ @Override
+ public void onMessage(final String message) {
+ System.out.println(message);
+
+ if (message == null) {
+ acknowledge(false);
+ return;
+ }
+
+ String token;
+ String username;
+
+ try {
+ token = gson.fromJson(message, JsonObject.class).get("token").getAsString();
+ username = jwtTokenUtils.getUsernameFromToken(token);
+ } catch (ExpiredJwtException e) {
+ System.err.println(e.getMessage());
+ acknowledge(false);
+ return;
+ } catch (Throwable ignored) {
+ System.out.println("Token format not valid");
+ acknowledge(false);
+ return;
+ }
+
+ final User user = userRepository.findByUsername(username);
+ if (user == null || jwtTokenUtils.isTokenExpired(token)) {
+ System.out.println("Token not valid");
+ acknowledge(false);
+ return;
+ }
+
+ // Here user is authenticated
+ session.removeMessageHandler(this);
+
+ // Add user-session pair in authorized list
+ authorizedSetter.accept(user, session);
+
+ // update client to acknowledge authentication
+ acknowledge(true);
+ }
+
+ private void acknowledge(boolean success) {
+ try {
+ session.getBasicRemote()
+ .sendText(gson.toJson(Map.of("authenticated", success)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
new file mode 100644
index 0000000..503667a
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
@@ -0,0 +1,54 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+import javax.websocket.server.ServerEndpointConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+import org.springframework.web.socket.server.standard.ServerEndpointRegistration;
+
+/** Configures the sensor socket and maps it to the /sensor-socket path */
+@Configuration
+public class SensorSocketConfig extends ServerEndpointConfig.Configurator {
+
+ private SensorSocketEndpoint instance;
+
+ @Autowired
+ public SensorSocketConfig(SensorSocketEndpoint instance) {
+ this.instance = instance;
+ }
+
+ /**
+ * Registers the sensor socket endpoint to the url /sensor-socket
+ *
+ * @return an endpoint registration object
+ */
+ @Bean
+ public ServerEndpointRegistration serverEndpointRegistration() {
+ return new ServerEndpointRegistration("/sensor-socket", instance);
+ }
+
+ /**
+ * Returns a new ServerEndpointExporter
+ *
+ * @return a new ServerEndpointExporter
+ */
+ @Bean
+ public ServerEndpointExporter endpointExporter() {
+ return new ServerEndpointExporter();
+ }
+
+ @Override
+ public T getEndpointInstance(Class endpointClass) throws InstantiationException {
+ try {
+ @SuppressWarnings("unchecked")
+ final T instance = (T) this.instance;
+ return instance;
+ } catch (ClassCastException e) {
+ final var e2 =
+ new InstantiationException("Cannot cast SensorSocketEndpoint to desired type");
+ e2.initCause(e);
+ throw e2;
+ }
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
new file mode 100644
index 0000000..d40e874
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
@@ -0,0 +1,84 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.socket;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.didThrow;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.gson.Gson;
+import java.util.*;
+import javax.websocket.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/** Endpoint of socket at URL /sensor-socket used to update the client with sensor information */
+@Component
+public class SensorSocketEndpoint extends Endpoint {
+
+ private Gson gson = new Gson();
+
+ private AuthenticationMessageListener authenticationMessageListener;
+
+ private Set unauthorizedClients = Collections.synchronizedSet(new HashSet<>());
+
+ private Multimap authorizedClients =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+
+ @Autowired
+ public SensorSocketEndpoint(AuthenticationMessageListener authenticationMessageListener) {
+ this.authenticationMessageListener = authenticationMessageListener;
+ }
+
+ /**
+ * Returns a synchronized set of socket sessions not yet authorized with a token
+ *
+ * @return a synchronized set of socket sessions not yet authorized with a token
+ */
+ public Set getUnauthorizedClients() {
+ return unauthorizedClients;
+ }
+
+ /**
+ * Returns a synchronized User to Session multimap with authorized sessions
+ *
+ * @return a synchronized User to Session multimap with authorized sessions
+ */
+ public Multimap getAuthorizedClients() {
+ return authorizedClients;
+ }
+
+ /**
+ * Given a message and a user, broadcasts that message in json to all associated clients and
+ * returns the number of successful transfers
+ *
+ * @param message the message to send
+ * @param u the user to which to send the message
+ * @return number of successful transfer
+ */
+ public long broadcast(Object message, User u) {
+ final Collection sessions = authorizedClients.get(u);
+ return sessions.stream()
+ .parallel()
+ .filter(didThrow(s -> s.getAsyncRemote().sendObject(gson.toJson(message))))
+ .count();
+ }
+
+ /**
+ * Handles the opening of a socket session with a client
+ *
+ * @param session the newly born session
+ * @param config endpoint configuration
+ */
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ unauthorizedClients.add(session);
+ session.addMessageHandler(
+ authenticationMessageListener.newHandler(
+ session,
+ (u, s) -> {
+ unauthorizedClients.remove(s);
+ authorizedClients.put(u, s);
+ }));
+ }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java
index d9fcf12..bc8719d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/Utils.java
@@ -1,14 +1,29 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.utils;
import java.util.List;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/** A class with a bunch of useful static methods */
-public class Utils {
+public final class Utils {
private Utils() {}
public static List toList(Iterable iterable) {
return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList());
}
+
+ public static Predicate didThrow(Function> consumer) {
+ return (t) -> {
+ try {
+ consumer.apply(t).get();
+ return true;
+ } catch (Throwable e) {
+ System.err.println(e.getMessage());
+ return false;
+ }
+ };
+ }
}