Skip to content

Commit c36a946

Browse files
committed
check registration status of stations for SOAP messages
reason: do not allow unknown stations access SteVe. basically, this change brings the same control that exists for WS/JSON to SOAP.
1 parent 4bec150 commit c36a946

2 files changed

Lines changed: 84 additions & 21 deletions

File tree

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package de.rwth.idsg.steve.ocpp.soap;
22

33
import de.rwth.idsg.steve.ocpp.OcppProtocol;
4+
import de.rwth.idsg.steve.repository.ChargePointRepository;
45
import de.rwth.idsg.steve.repository.OcppServerRepository;
56
import de.rwth.idsg.steve.repository.impl.ChargePointRepositoryImpl;
67
import lombok.extern.slf4j.Slf4j;
8+
import org.apache.cxf.binding.soap.Soap12;
9+
import org.apache.cxf.binding.soap.SoapFault;
710
import org.apache.cxf.interceptor.Fault;
811
import org.apache.cxf.message.Message;
912
import org.apache.cxf.message.MessageContentsList;
@@ -17,12 +20,15 @@
1720
import org.springframework.beans.factory.annotation.Autowired;
1821
import org.springframework.stereotype.Component;
1922

23+
import javax.xml.namespace.QName;
2024
import java.util.concurrent.ScheduledExecutorService;
2125

2226
import static org.apache.cxf.ws.addressing.JAXWSAConstants.ADDRESSING_PROPERTIES_INBOUND;
2327

2428
/**
25-
* Intercepts incoming OCPP messages to update the endpoint address ("From" field of the WS-A header) in DB.
29+
* 1. Checks the registration status of a station for operations other than BootNotification.
30+
*
31+
* 2. Intercepts incoming OCPP messages to update the endpoint address ("From" field of the WS-A header) in DB.
2632
* And the absence of the field is not a deal breaker anymore. But, as a side effect, the user will not be able
2733
* to send commands to the charging station, since the DB call to list the charge points will filter it out. See
2834
* {@link ChargePointRepositoryImpl#getChargePointSelect(OcppProtocol)}.
@@ -35,8 +41,10 @@
3541
public class FromAddressInterceptor extends AbstractPhaseInterceptor<Message> {
3642

3743
@Autowired private OcppServerRepository ocppServerRepository;
44+
@Autowired private ChargePointRepository chargePointRepository;
3845
@Autowired private ScheduledExecutorService executorService;
3946

47+
private static final String BOOT_OPERATION_NAME = "BootNotification";
4048
private static final String CHARGEBOX_ID_HEADER = "ChargeBoxIdentity";
4149

4250
public FromAddressInterceptor() {
@@ -45,36 +53,48 @@ public FromAddressInterceptor() {
4553

4654
@Override
4755
public void handleMessage(Message message) throws Fault {
48-
executorService.execute(() -> handleMessageInternal(message));
49-
}
56+
String chargeBoxId = getChargeBoxId(message);
5057

51-
private void handleMessageInternal(Message message) {
52-
try {
53-
String chargeBoxId = getChargeBoxId(message);
54-
String endpointAddress = getEndpointAddress(message);
58+
// -------------------------------------------------------------------------
59+
// 1. check registration for operations other than BootNotification
60+
// -------------------------------------------------------------------------
5561

56-
if (chargeBoxId != null && endpointAddress != null) {
57-
ocppServerRepository.updateEndpointAddress(chargeBoxId, endpointAddress);
62+
QName opName = message.getExchange().getBindingOperationInfo().getOperationInfo().getName();
63+
64+
if (!BOOT_OPERATION_NAME.equals(opName.getLocalPart())) {
65+
if (!chargePointRepository.isRegistered(chargeBoxId)) {
66+
throw createAuthFault(opName);
5867
}
59-
} catch (Exception e) {
60-
log.error("Exception occurred", e);
6168
}
69+
70+
// -------------------------------------------------------------------------
71+
// 2. update endpoint
72+
// -------------------------------------------------------------------------
73+
74+
executorService.execute(() -> {
75+
try {
76+
String endpointAddress = getEndpointAddress(message);
77+
if (endpointAddress != null) {
78+
ocppServerRepository.updateEndpointAddress(chargeBoxId, endpointAddress);
79+
}
80+
} catch (Exception e) {
81+
log.error("Exception occurred", e);
82+
}
83+
});
6284
}
6385

6486
private String getChargeBoxId(Message message) {
6587
MessageContentsList lst = MessageContentsList.getContentsList(message);
66-
if (lst == null) {
67-
return null;
68-
}
69-
70-
MessageInfo mi = (MessageInfo) message.get("org.apache.cxf.service.model.MessageInfo");
71-
for (MessagePartInfo mpi : mi.getMessageParts()) {
72-
if (CHARGEBOX_ID_HEADER.equals(mpi.getName().getLocalPart())) {
73-
return (String) lst.get(mpi);
88+
if (lst != null) {
89+
MessageInfo mi = (MessageInfo) message.get("org.apache.cxf.service.model.MessageInfo");
90+
for (MessagePartInfo mpi : mi.getMessageParts()) {
91+
if (CHARGEBOX_ID_HEADER.equals(mpi.getName().getLocalPart())) {
92+
return (String) lst.get(mpi);
93+
}
7494
}
7595
}
76-
77-
return null;
96+
// should not happen
97+
throw createSpecFault(message.getExchange().getBindingOperationInfo().getOperationInfo().getName());
7898
}
7999

80100
private String getEndpointAddress(Message message) {
@@ -90,4 +110,20 @@ private String getEndpointAddress(Message message) {
90110
return from.getAddress().getValue();
91111
}
92112
}
113+
114+
private static SoapFault createAuthFault(QName qName) {
115+
// as defined by OCPP spec
116+
String message = "Sender failed authentication or is not authorized to use the requested operation.";
117+
SoapFault sf = new SoapFault(message, Soap12.getInstance().getSender());
118+
sf.addSubCode(new QName(qName.getNamespaceURI(), "SecurityError"));
119+
return sf;
120+
}
121+
122+
private static SoapFault createSpecFault(QName qName) {
123+
// as defined by OCPP spec
124+
String message = "Sender's message does not comply with protocol specification.";
125+
SoapFault sf = new SoapFault(message, Soap12.getInstance().getSender());
126+
sf.addSubCode(new QName(qName.getNamespaceURI(), "ProtocolError"));
127+
return sf;
128+
}
93129
}

src/test/java/de/rwth/idsg/steve/OperationalTestSoapOCPP16.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package de.rwth.idsg.steve;
22

3+
import de.rwth.idsg.steve.ocpp.OcppProtocol;
4+
import de.rwth.idsg.steve.ocpp.soap.FromAddressInterceptor;
5+
import de.rwth.idsg.steve.ocpp.ws.OcppWebSocketUpgrader;
36
import de.rwth.idsg.steve.repository.ReservationStatus;
47
import de.rwth.idsg.steve.repository.dto.ChargePoint;
58
import de.rwth.idsg.steve.repository.dto.ConnectorStatus;
69
import de.rwth.idsg.steve.repository.dto.Reservation;
710
import de.rwth.idsg.steve.repository.dto.Transaction;
811
import de.rwth.idsg.steve.repository.dto.TransactionDetails;
12+
import de.rwth.idsg.steve.service.CentralSystemService16_Service;
913
import de.rwth.idsg.steve.utils.__DatabasePreparer__;
1014
import jooq.steve.db.tables.records.TransactionRecord;
1115
import lombok.extern.slf4j.Slf4j;
@@ -38,6 +42,7 @@
3842
import org.junit.BeforeClass;
3943
import org.junit.Test;
4044

45+
import javax.xml.ws.soap.SOAPFaultException;
4146
import java.util.Arrays;
4247
import java.util.List;
4348

@@ -97,6 +102,28 @@ public void testUnregisteredCP() {
97102
Assert.assertNotEquals(RegistrationStatus.ACCEPTED, boot.getStatus());
98103
}
99104

105+
/**
106+
* Reason: We started to check registration status by intercepting every SOAP message other than BootNotification
107+
* in {@link FromAddressInterceptor} and throw exception if station is not registered and auto-register is
108+
* disabled (and therefore early-exit the processing pipeline of the message).
109+
*
110+
* In case of BootNotification, the expected behaviour is to set RegistrationStatus.REJECTED in response, as done
111+
* by {@link CentralSystemService16_Service#bootNotification(BootNotificationRequest, String, OcppProtocol)}.
112+
* Therefore, no exception. This case is tested by {@link OperationalTestSoapOCPP16#testUnregisteredCP()} already.
113+
*
114+
* WS/JSON stations cannot connect at all if they are not registered, as ensured by {@link OcppWebSocketUpgrader}.
115+
*/
116+
@Test(expected = SOAPFaultException.class)
117+
public void testUnregisteredCPWithInterceptor() {
118+
Assert.assertFalse(SteveConfiguration.CONFIG.getOcpp().isAutoRegisterUnknownStations());
119+
120+
CentralSystemService client = getForOcpp16(path);
121+
122+
client.authorize(
123+
new AuthorizeRequest().withIdTag(REGISTERED_OCPP_TAG),
124+
getRandomString());
125+
}
126+
100127
@Test
101128
public void testRegisteredCP() {
102129
CentralSystemService client = getForOcpp16(path);

0 commit comments

Comments
 (0)