Skip to content

Commit ae45361

Browse files
authored
Merge pull request ChargeTimeEU#163 from dimaa6/serverauthcheck
Client authentication on server side via callback
2 parents f6af5d3 + 2821036 commit ae45361

File tree

5 files changed

+170
-74
lines changed

5 files changed

+170
-74
lines changed

OCPP-J/src/main/java/eu/chargetime/ocpp/WebSocketListener.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ of this software and associated documentation files (the "Software"), to deal
3030
import java.io.IOException;
3131
import java.net.ConnectException;
3232
import java.net.InetSocketAddress;
33+
import java.nio.charset.StandardCharsets;
3334
import java.util.Arrays;
35+
import java.util.Base64;
3436
import java.util.List;
3537
import java.util.Map;
3638
import java.util.concurrent.ConcurrentHashMap;
3739
import org.java_websocket.WebSocket;
3840
import org.java_websocket.drafts.Draft;
41+
import org.java_websocket.exceptions.InvalidDataException;
3942
import org.java_websocket.handshake.ClientHandshake;
43+
import org.java_websocket.handshake.ServerHandshakeBuilder;
4044
import org.java_websocket.server.WebSocketServer;
4145
import org.slf4j.Logger;
4246
import org.slf4j.LoggerFactory;
@@ -46,6 +50,9 @@ public class WebSocketListener implements Listener {
4650

4751
private static final int TIMEOUT_IN_MILLIS = 10000;
4852

53+
private static final int OCPPJ_CP_MIN_PASSWORD_LENGTH = 16;
54+
private static final int OCPPJ_CP_MAX_PASSWORD_LENGTH = 20;
55+
4956
private final ISessionFactory sessionFactory;
5057
private final List<Draft> drafts;
5158

@@ -109,6 +116,50 @@ public void relay(String message) {
109116
sessionFactory.createSession(new JSONCommunicator(receiver)), information);
110117
}
111118

119+
@Override
120+
public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket webSocket, Draft draft,
121+
ClientHandshake clientHandshake) throws InvalidDataException {
122+
SessionInformation information =
123+
new SessionInformation.Builder()
124+
.Identifier(clientHandshake.getResourceDescriptor())
125+
.InternetAddress(webSocket.getRemoteSocketAddress())
126+
.build();
127+
128+
String username = null;
129+
byte[] password = null;
130+
if (clientHandshake.hasFieldValue("Authorization")) {
131+
String authorization = clientHandshake.getFieldValue("Authorization");
132+
if (authorization != null && authorization.toLowerCase().startsWith("basic")) {
133+
// Authorization: Basic base64credentials
134+
String base64Credentials = authorization.substring("Basic".length()).trim();
135+
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
136+
// split credentials on username and password
137+
for (int i = 0; i < credDecoded.length; i++) {
138+
if (credDecoded[i] == ':') {
139+
username = new String(Arrays.copyOfRange(credDecoded, 0, i), StandardCharsets.UTF_8);
140+
if (i + 1 < credDecoded.length) {
141+
password = Arrays.copyOfRange(credDecoded, i + 1, credDecoded.length);
142+
}
143+
break;
144+
}
145+
}
146+
}
147+
if (password == null || password.length < OCPPJ_CP_MIN_PASSWORD_LENGTH || password.length > OCPPJ_CP_MAX_PASSWORD_LENGTH)
148+
throw new InvalidDataException(401, "Invalid password length");
149+
}
150+
151+
try {
152+
handler.authenticateSession(information, username, password);
153+
}
154+
catch (AuthenticationException e) {
155+
throw new InvalidDataException(e.getErrorCode(), e.getMessage());
156+
}
157+
catch (Exception e) {
158+
throw new InvalidDataException(401, e.getMessage());
159+
}
160+
return super.onWebsocketHandshakeReceivedAsServer(webSocket, draft, clientHandshake);
161+
}
162+
112163
@Override
113164
public void onClose(WebSocket webSocket, int code, String reason, boolean remote) {
114165
logger.debug(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package eu.chargetime.ocpp;
2+
3+
import java.io.Serializable;
4+
5+
public class AuthenticationException extends Exception implements Serializable {
6+
private static final long serialVersionUID = -2323276402779073385L;
7+
private final int errorcode;
8+
9+
public AuthenticationException(int errorcode) {
10+
this.errorcode = errorcode;
11+
}
12+
13+
public AuthenticationException(int errorcode, String s) {
14+
super(s);
15+
this.errorcode = errorcode;
16+
}
17+
18+
public AuthenticationException(int errorcode, Throwable t) {
19+
super(t);
20+
this.errorcode = errorcode;
21+
}
22+
23+
public AuthenticationException(int errorcode, String s, Throwable t) {
24+
super(s, t);
25+
this.errorcode = errorcode;
26+
}
27+
28+
public int getErrorCode() {
29+
return this.errorcode;
30+
}
31+
}

ocpp-common/src/main/java/eu/chargetime/ocpp/ListenerEvents.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ of this software and associated documentation files (the "Software"), to deal
2828
import eu.chargetime.ocpp.model.SessionInformation;
2929

3030
public interface ListenerEvents {
31+
void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException;
3132
void newSession(ISession session, SessionInformation information);
3233
}

ocpp-common/src/main/java/eu/chargetime/ocpp/Server.java

Lines changed: 85 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ of this software and associated documentation files (the "Software"), to deal
3333
import java.util.UUID;
3434
import java.util.concurrent.CompletableFuture;
3535
import java.util.concurrent.ConcurrentHashMap;
36+
37+
import eu.chargetime.ocpp.model.SessionInformation;
3638
import org.slf4j.Logger;
3739
import org.slf4j.LoggerFactory;
3840

@@ -78,80 +80,89 @@ public void open(String hostname, int port, ServerEvents serverEvents) {
7880
listener.open(
7981
hostname,
8082
port,
81-
(session, information) -> {
82-
session.accept(
83-
new SessionEvents() {
84-
@Override
85-
public void handleConfirmation(String uniqueId, Confirmation confirmation) {
86-
87-
Optional<CompletableFuture<Confirmation>> promiseOptional =
88-
promiseRepository.getPromise(uniqueId);
89-
if (promiseOptional.isPresent()) {
90-
promiseOptional.get().complete(confirmation);
91-
promiseRepository.removePromise(uniqueId);
92-
} else {
93-
logger.debug("Promise not found for confirmation {}", confirmation);
94-
}
95-
}
96-
97-
@Override
98-
public Confirmation handleRequest(Request request)
99-
throws UnsupportedFeatureException {
100-
Optional<Feature> featureOptional = featureRepository.findFeature(request);
101-
if (featureOptional.isPresent()) {
102-
Optional<UUID> sessionIdOptional = getSessionID(session);
103-
if (sessionIdOptional.isPresent()) {
104-
return featureOptional.get().handleRequest(sessionIdOptional.get(), request);
105-
} else {
106-
logger.error(
107-
"Unable to handle request ({}), the active session was not found.",
108-
request);
109-
throw new IllegalStateException("Active session not found");
110-
}
111-
} else {
112-
throw new UnsupportedFeatureException();
113-
}
114-
}
115-
116-
@Override
117-
public void handleError(
118-
String uniqueId, String errorCode, String errorDescription, Object payload) {
119-
Optional<CompletableFuture<Confirmation>> promiseOptional =
120-
promiseRepository.getPromise(uniqueId);
121-
if (promiseOptional.isPresent()) {
122-
promiseOptional
123-
.get()
124-
.completeExceptionally(
125-
new CallErrorException(errorCode, errorDescription, payload));
126-
promiseRepository.removePromise(uniqueId);
127-
} else {
128-
logger.debug("Promise not found for error {}", errorDescription);
129-
}
130-
}
131-
132-
@Override
133-
public void handleConnectionClosed() {
134-
Optional<UUID> sessionIdOptional = getSessionID(session);
135-
if (sessionIdOptional.isPresent()) {
136-
serverEvents.lostSession(sessionIdOptional.get());
137-
sessions.remove(sessionIdOptional.get());
138-
} else {
139-
logger.warn("Active session not found");
140-
}
141-
}
142-
143-
@Override
144-
public void handleConnectionOpened() {}
145-
});
146-
147-
sessions.put(session.getSessionId(), session);
148-
149-
Optional<UUID> sessionIdOptional = getSessionID(session);
150-
if (sessionIdOptional.isPresent()) {
151-
serverEvents.newSession(sessionIdOptional.get(), information);
152-
logger.debug("Session created: {}", session.getSessionId());
153-
} else {
154-
throw new IllegalStateException("Failed to create a session");
83+
new ListenerEvents() {
84+
85+
@Override
86+
public void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException {
87+
serverEvents.authenticateSession(information, username, password);
88+
}
89+
90+
@Override
91+
public void newSession(ISession session, SessionInformation information) {
92+
session.accept(
93+
new SessionEvents() {
94+
@Override
95+
public void handleConfirmation(String uniqueId, Confirmation confirmation) {
96+
97+
Optional<CompletableFuture<Confirmation>> promiseOptional =
98+
promiseRepository.getPromise(uniqueId);
99+
if (promiseOptional.isPresent()) {
100+
promiseOptional.get().complete(confirmation);
101+
promiseRepository.removePromise(uniqueId);
102+
} else {
103+
logger.debug("Promise not found for confirmation {}", confirmation);
104+
}
105+
}
106+
107+
@Override
108+
public Confirmation handleRequest(Request request)
109+
throws UnsupportedFeatureException {
110+
Optional<Feature> featureOptional = featureRepository.findFeature(request);
111+
if (featureOptional.isPresent()) {
112+
Optional<UUID> sessionIdOptional = getSessionID(session);
113+
if (sessionIdOptional.isPresent()) {
114+
return featureOptional.get().handleRequest(sessionIdOptional.get(), request);
115+
} else {
116+
logger.error(
117+
"Unable to handle request ({}), the active session was not found.",
118+
request);
119+
throw new IllegalStateException("Active session not found");
120+
}
121+
} else {
122+
throw new UnsupportedFeatureException();
123+
}
124+
}
125+
126+
@Override
127+
public void handleError(
128+
String uniqueId, String errorCode, String errorDescription, Object payload) {
129+
Optional<CompletableFuture<Confirmation>> promiseOptional =
130+
promiseRepository.getPromise(uniqueId);
131+
if (promiseOptional.isPresent()) {
132+
promiseOptional
133+
.get()
134+
.completeExceptionally(
135+
new CallErrorException(errorCode, errorDescription, payload));
136+
promiseRepository.removePromise(uniqueId);
137+
} else {
138+
logger.debug("Promise not found for error {}", errorDescription);
139+
}
140+
}
141+
142+
@Override
143+
public void handleConnectionClosed() {
144+
Optional<UUID> sessionIdOptional = getSessionID(session);
145+
if (sessionIdOptional.isPresent()) {
146+
serverEvents.lostSession(sessionIdOptional.get());
147+
sessions.remove(sessionIdOptional.get());
148+
} else {
149+
logger.warn("Active session not found");
150+
}
151+
}
152+
153+
@Override
154+
public void handleConnectionOpened() {}
155+
});
156+
157+
sessions.put(session.getSessionId(), session);
158+
159+
Optional<UUID> sessionIdOptional = getSessionID(session);
160+
if (sessionIdOptional.isPresent()) {
161+
serverEvents.newSession(sessionIdOptional.get(), information);
162+
logger.debug("Session created: {}", session.getSessionId());
163+
} else {
164+
throw new IllegalStateException("Failed to create a session");
165+
}
155166
}
156167
});
157168
}

ocpp-common/src/main/java/eu/chargetime/ocpp/ServerEvents.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ of this software and associated documentation files (the "Software"), to deal
2929
import java.util.UUID;
3030

3131
public interface ServerEvents {
32+
default void authenticateSession(SessionInformation information, String username, byte[] password) throws AuthenticationException {}
33+
3234
void newSession(UUID sessionIndex, SessionInformation information);
3335

3436
void lostSession(UUID sessionIndex);

0 commit comments

Comments
 (0)