diff --git a/OCPP-J/src/main/java/eu/chargetime/ocpp/WebSocketListener.java b/OCPP-J/src/main/java/eu/chargetime/ocpp/WebSocketListener.java index 9950501ff..03e6b9981 100644 --- a/OCPP-J/src/main/java/eu/chargetime/ocpp/WebSocketListener.java +++ b/OCPP-J/src/main/java/eu/chargetime/ocpp/WebSocketListener.java @@ -30,13 +30,17 @@ of this software and associated documentation files (the "Software"), to deal import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.java_websocket.WebSocket; import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; import org.java_websocket.server.WebSocketServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +50,9 @@ public class WebSocketListener implements Listener { private static final int TIMEOUT_IN_MILLIS = 10000; + private static final int OCPPJ_CP_MIN_PASSWORD_LENGTH = 16; + private static final int OCPPJ_CP_MAX_PASSWORD_LENGTH = 20; + private final ISessionFactory sessionFactory; private final List drafts; @@ -109,6 +116,50 @@ public void relay(String message) { sessionFactory.createSession(new JSONCommunicator(receiver)), information); } + @Override + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket webSocket, Draft draft, + ClientHandshake clientHandshake) throws InvalidDataException { + SessionInformation information = + new SessionInformation.Builder() + .Identifier(clientHandshake.getResourceDescriptor()) + .InternetAddress(webSocket.getRemoteSocketAddress()) + .build(); + + String username = null; + byte[] password = null; + if (clientHandshake.hasFieldValue("Authorization")) { + String authorization = clientHandshake.getFieldValue("Authorization"); + if (authorization != null && authorization.toLowerCase().startsWith("basic")) { + // Authorization: Basic base64credentials + String base64Credentials = authorization.substring("Basic".length()).trim(); + byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); + // split credentials on username and password + for (int i = 0; i < credDecoded.length; i++) { + if (credDecoded[i] == ':') { + username = new String(Arrays.copyOfRange(credDecoded, 0, i), StandardCharsets.UTF_8); + if (i + 1 < credDecoded.length) { + password = Arrays.copyOfRange(credDecoded, i + 1, credDecoded.length); + } + break; + } + } + } + if (password == null || password.length < OCPPJ_CP_MIN_PASSWORD_LENGTH || password.length > OCPPJ_CP_MAX_PASSWORD_LENGTH) + throw new InvalidDataException(401, "Invalid password length"); + } + + try { + handler.authenticateSession(information, username, password); + } + catch (AuthenticationException e) { + throw new InvalidDataException(e.getErrorCode(), e.getMessage()); + } + catch (Exception e) { + throw new InvalidDataException(401, e.getMessage()); + } + return super.onWebsocketHandshakeReceivedAsServer(webSocket, draft, clientHandshake); + } + @Override public void onClose(WebSocket webSocket, int code, String reason, boolean remote) { logger.debug( diff --git a/ocpp-common/src/main/java/eu/chargetime/ocpp/AuthenticationException.java b/ocpp-common/src/main/java/eu/chargetime/ocpp/AuthenticationException.java new file mode 100644 index 000000000..29d774499 --- /dev/null +++ b/ocpp-common/src/main/java/eu/chargetime/ocpp/AuthenticationException.java @@ -0,0 +1,31 @@ +package eu.chargetime.ocpp; + +import java.io.Serializable; + +public class AuthenticationException extends Exception implements Serializable { + private static final long serialVersionUID = -2323276402779073385L; + private final int errorcode; + + public AuthenticationException(int errorcode) { + this.errorcode = errorcode; + } + + public AuthenticationException(int errorcode, String s) { + super(s); + this.errorcode = errorcode; + } + + public AuthenticationException(int errorcode, Throwable t) { + super(t); + this.errorcode = errorcode; + } + + public AuthenticationException(int errorcode, String s, Throwable t) { + super(s, t); + this.errorcode = errorcode; + } + + public int getErrorCode() { + return this.errorcode; + } +} diff --git a/ocpp-common/src/main/java/eu/chargetime/ocpp/ListenerEvents.java b/ocpp-common/src/main/java/eu/chargetime/ocpp/ListenerEvents.java index 8bf2ae252..81eb7c01a 100644 --- a/ocpp-common/src/main/java/eu/chargetime/ocpp/ListenerEvents.java +++ b/ocpp-common/src/main/java/eu/chargetime/ocpp/ListenerEvents.java @@ -28,5 +28,6 @@ of this software and associated documentation files (the "Software"), to deal import eu.chargetime.ocpp.model.SessionInformation; public interface ListenerEvents { + void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException; void newSession(ISession session, SessionInformation information); } diff --git a/ocpp-common/src/main/java/eu/chargetime/ocpp/Server.java b/ocpp-common/src/main/java/eu/chargetime/ocpp/Server.java index 504d83069..1e4f60cb8 100644 --- a/ocpp-common/src/main/java/eu/chargetime/ocpp/Server.java +++ b/ocpp-common/src/main/java/eu/chargetime/ocpp/Server.java @@ -33,6 +33,8 @@ of this software and associated documentation files (the "Software"), to deal import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; + +import eu.chargetime.ocpp.model.SessionInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,80 +80,89 @@ public void open(String hostname, int port, ServerEvents serverEvents) { listener.open( hostname, port, - (session, information) -> { - session.accept( - new SessionEvents() { - @Override - public void handleConfirmation(String uniqueId, Confirmation confirmation) { - - Optional> promiseOptional = - promiseRepository.getPromise(uniqueId); - if (promiseOptional.isPresent()) { - promiseOptional.get().complete(confirmation); - promiseRepository.removePromise(uniqueId); - } else { - logger.debug("Promise not found for confirmation {}", confirmation); - } - } - - @Override - public Confirmation handleRequest(Request request) - throws UnsupportedFeatureException { - Optional featureOptional = featureRepository.findFeature(request); - if (featureOptional.isPresent()) { - Optional sessionIdOptional = getSessionID(session); - if (sessionIdOptional.isPresent()) { - return featureOptional.get().handleRequest(sessionIdOptional.get(), request); - } else { - logger.error( - "Unable to handle request ({}), the active session was not found.", - request); - throw new IllegalStateException("Active session not found"); - } - } else { - throw new UnsupportedFeatureException(); - } - } - - @Override - public void handleError( - String uniqueId, String errorCode, String errorDescription, Object payload) { - Optional> promiseOptional = - promiseRepository.getPromise(uniqueId); - if (promiseOptional.isPresent()) { - promiseOptional - .get() - .completeExceptionally( - new CallErrorException(errorCode, errorDescription, payload)); - promiseRepository.removePromise(uniqueId); - } else { - logger.debug("Promise not found for error {}", errorDescription); - } - } - - @Override - public void handleConnectionClosed() { - Optional sessionIdOptional = getSessionID(session); - if (sessionIdOptional.isPresent()) { - serverEvents.lostSession(sessionIdOptional.get()); - sessions.remove(sessionIdOptional.get()); - } else { - logger.warn("Active session not found"); - } - } - - @Override - public void handleConnectionOpened() {} - }); - - sessions.put(session.getSessionId(), session); - - Optional sessionIdOptional = getSessionID(session); - if (sessionIdOptional.isPresent()) { - serverEvents.newSession(sessionIdOptional.get(), information); - logger.debug("Session created: {}", session.getSessionId()); - } else { - throw new IllegalStateException("Failed to create a session"); + new ListenerEvents() { + + @Override + public void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException { + serverEvents.authenticateSession(information, username, password); + } + + @Override + public void newSession(ISession session, SessionInformation information) { + session.accept( + new SessionEvents() { + @Override + public void handleConfirmation(String uniqueId, Confirmation confirmation) { + + Optional> promiseOptional = + promiseRepository.getPromise(uniqueId); + if (promiseOptional.isPresent()) { + promiseOptional.get().complete(confirmation); + promiseRepository.removePromise(uniqueId); + } else { + logger.debug("Promise not found for confirmation {}", confirmation); + } + } + + @Override + public Confirmation handleRequest(Request request) + throws UnsupportedFeatureException { + Optional featureOptional = featureRepository.findFeature(request); + if (featureOptional.isPresent()) { + Optional sessionIdOptional = getSessionID(session); + if (sessionIdOptional.isPresent()) { + return featureOptional.get().handleRequest(sessionIdOptional.get(), request); + } else { + logger.error( + "Unable to handle request ({}), the active session was not found.", + request); + throw new IllegalStateException("Active session not found"); + } + } else { + throw new UnsupportedFeatureException(); + } + } + + @Override + public void handleError( + String uniqueId, String errorCode, String errorDescription, Object payload) { + Optional> promiseOptional = + promiseRepository.getPromise(uniqueId); + if (promiseOptional.isPresent()) { + promiseOptional + .get() + .completeExceptionally( + new CallErrorException(errorCode, errorDescription, payload)); + promiseRepository.removePromise(uniqueId); + } else { + logger.debug("Promise not found for error {}", errorDescription); + } + } + + @Override + public void handleConnectionClosed() { + Optional sessionIdOptional = getSessionID(session); + if (sessionIdOptional.isPresent()) { + serverEvents.lostSession(sessionIdOptional.get()); + sessions.remove(sessionIdOptional.get()); + } else { + logger.warn("Active session not found"); + } + } + + @Override + public void handleConnectionOpened() {} + }); + + sessions.put(session.getSessionId(), session); + + Optional sessionIdOptional = getSessionID(session); + if (sessionIdOptional.isPresent()) { + serverEvents.newSession(sessionIdOptional.get(), information); + logger.debug("Session created: {}", session.getSessionId()); + } else { + throw new IllegalStateException("Failed to create a session"); + } } }); } diff --git a/ocpp-common/src/main/java/eu/chargetime/ocpp/ServerEvents.java b/ocpp-common/src/main/java/eu/chargetime/ocpp/ServerEvents.java index 8b042111d..4d8a5d0b4 100644 --- a/ocpp-common/src/main/java/eu/chargetime/ocpp/ServerEvents.java +++ b/ocpp-common/src/main/java/eu/chargetime/ocpp/ServerEvents.java @@ -29,6 +29,8 @@ of this software and associated documentation files (the "Software"), to deal import java.util.UUID; public interface ServerEvents { + default void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException {} + void newSession(UUID sessionIndex, SessionInformation information); void lostSession(UUID sessionIndex);