/*
* Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.openmuc.openiec61850;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import javax.net.ServerSocketFactory;
import org.openmuc.josistack.AcseAssociation;
import org.openmuc.josistack.ServerAcseSap;
/**
* The ServerSap
class represents the IEC 61850 service access point for server applications. It
* corresponds to the AccessPoint defined in the ICD/SCL file. A server application that is to listen for client
* connections should first get an instance of ServerSap
using the static function
* ServerSap.getSapsFromSclFile(). Next all the necessary configuration parameters can be set. Finally the
* startListening
function is called to listen for client associations. Changing properties of a ServerSap
* after starting to listen is not recommended and has unknown effects.
*/
public final class ServerSap {
static final int MINIMUM_MMS_PDU_SIZE = 64;
private static final int MAXIMUM_MMS_PDU_SIZE = 65000;
private int proposedMaxMmsPduSize = 65000;
private int proposedMaxServOutstandingCalling = 5;
private int proposedMaxServOutstandingCalled = 5;
private int proposedDataStructureNestingLevel = 10;
byte[] servicesSupportedCalled = new byte[] { (byte) 0xee, 0x1c, 0, 0, 0x04, 0x08, 0, 0, 0x79, (byte) 0xef, 0x18 };
byte[] cbbBitString = { (byte) (0xfb), 0x00 };
private int maxAssociations = 100;
ServerEventListener serverEventListener;
private ServerAcseSap acseSap;
private final String name;
private int port = 102;
private int backlog = 0;
private InetAddress bindAddr = null;
private ServerSocketFactory serverSocketFactory = null;
Timer timer;
List associations = new ArrayList<>();
boolean listening = false;
public final ServerModel serverModel;
public static List getSapsFromSclFile(String sclFilePath) throws SclParseException {
SclParser sclParserObject = new SclParser();
sclParserObject.parse(sclFilePath);
return sclParserObject.getServerSaps();
}
public static List getSapsFromSclFile(InputStream sclFileStream) throws SclParseException {
SclParser sclParserObject = new SclParser();
sclParserObject.parse(sclFileStream);
return sclParserObject.getServerSaps();
}
/**
* Creates a ServerSap.
*
* @param port
* local port to listen on for new connections
* @param backlog
* The maximum queue length for incoming connection indications (a request to connect) is set to the
* backlog parameter. If a connection indication arrives when the queue is full, the connection is
* refused. Set to 0 or less for the default value.
* @param bindAddr
* local IP address to bind to, pass null to bind to all
* @param serverSocketFactory
* the factory class to generate the ServerSocket. Could be used to create SSLServerSockets. null =
* default
*
*/
ServerSap(int port, int backlog, InetAddress bindAddr, ServerModel serverModel, String name,
ServerSocketFactory serverSocketFactory) {
this.port = port;
this.backlog = backlog;
this.bindAddr = bindAddr;
this.serverSocketFactory = serverSocketFactory;
this.name = name;
this.serverModel = serverModel;
}
/**
* Sets local port to listen on for new connections.
*
* @param port
* local port to listen on for new connections
*/
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return port;
}
/**
* Sets the maximum queue length for incoming connection indications (a request to connect) is set to the backlog
* parameter. If a connection indication arrives when the queue is full, the connection is refused. Set to 0 or less
* for the default value.
*
* @param backlog
* the maximum queue length for incoming connections.
*/
public void setBacklog(int backlog) {
this.backlog = backlog;
}
public int getBacklog() {
return backlog;
}
/**
* Sets the local IP address to bind to, pass null to bind to all
*
* @param bindAddr
* the local IP address to bind to
*/
public void setBindAddress(InetAddress bindAddr) {
this.bindAddr = bindAddr;
}
public InetAddress getBindAddress() {
return bindAddr;
}
/**
* Returns the name of the ServerSap / AccessPoint as specified in the SCL file.
*
* @return the name.
*/
public String getName() {
return name;
}
/**
* Sets the factory class to generate the ServerSocket. The ServerSocketFactory could be used to create
* SSLServerSockets. Set to null
to use ServerSocketFactory.getDefault()
.
*
* @param serverSocketFactory
* the factory class to generate the ServerSocket.
*/
public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
this.serverSocketFactory = serverSocketFactory;
}
/**
* Sets the maximum MMS PDU size in bytes that the server will support. If the client requires the use of a smaller
* maximum MMS PDU size, then the smaller size will be accepted by the server. The default size is 65000.
*
* @param size
* cannot be less than 64. The upper limit is 65000 so that segmentation at the lower transport layer is
* avoided. The Transport Layer's maximum PDU size is 65531.
*/
public void setMaxMmsPduSize(int size) {
if (size >= MINIMUM_MMS_PDU_SIZE && size <= MAXIMUM_MMS_PDU_SIZE) {
proposedMaxMmsPduSize = size;
}
else {
throw new IllegalArgumentException("maximum size is out of bound");
}
}
/**
* Gets the maximum MMS PDU size.
*
* @return the maximum MMS PDU size.
*/
public int getMaxMmsPduSize() {
return proposedMaxMmsPduSize;
}
/**
* Set the maximum number of associations that are allowed in parallel by the server.
*
* @param maxAssociations
* the number of associations allowed (default is 100)
*/
public void setMaxAssociations(int maxAssociations) {
this.maxAssociations = maxAssociations;
}
/**
* Sets the message fragment timeout. This is the timeout that the socket timeout is set to after the first byte of
* a message has been received. If such a timeout is thrown, the association/socket is closed.
*
* @param timeout
* the message fragment timeout in milliseconds. The default is 60000.
*/
public void setMessageFragmentTimeout(int timeout) {
acseSap.serverTSap.setMessageFragmentTimeout(timeout);
}
/**
* Sets the ProposedMaxServOutstandingCalling parameter. The given parameter has no affect on the functionality of
* this server.
*
* @param maxCalling
* the ProposedMaxServOutstandingCalling parameter. The default is 5.
*/
public void setProposedMaxServOutstandingCalling(int maxCalling) {
proposedMaxServOutstandingCalling = maxCalling;
}
/**
* Gets the ProposedMaxServOutstandingCalling parameter.
*
* @return the ProposedMaxServOutstandingCalling parameter.
*/
public int getProposedMaxServOutstandingCalling() {
return proposedMaxServOutstandingCalling;
}
/**
* Sets the ProposedMaxServOutstandingCalled parameter.The given parameter has no affect on the functionality of
* this server.
*
* @param maxCalled
* the ProposedMaxServOutstandingCalled parameter. The default is 5.
*/
public void setProposedMaxServOutstandingCalled(int maxCalled) {
proposedMaxServOutstandingCalled = maxCalled;
}
/**
* Gets the ProposedMaxServOutstandingCalled parameter.
*
* @return the ProposedMaxServOutstandingCalled parameter.
*/
public int getProposedMaxServOutstandingCalled() {
return proposedMaxServOutstandingCalled;
}
/**
* Sets the ProposedDataStructureNestingLevel parameter. The given parameter has no affect on the functionality of
* this server.runServer
*
* @param nestingLevel
* the ProposedDataStructureNestingLevel parameter. The default is 10.
*/
public void setProposedDataStructureNestingLevel(int nestingLevel) {
proposedDataStructureNestingLevel = nestingLevel;
}
/**
* Gets the ProposedDataStructureNestingLevel parameter.
*
* @return the ProposedDataStructureNestingLevel parameter.
*/
public int getProposedDataStructureNestingLevel() {
return proposedDataStructureNestingLevel;
}
/**
* Sets the SevicesSupportedCalled parameter. The given parameter has no affect on the functionality of this server.
*
* @param services
* the ServicesSupportedCalled parameter
*/
public void setServicesSupportedCalled(byte[] services) {
if (services.length != 11) {
throw new IllegalArgumentException("The services parameter needs to be of lenth 11");
}
servicesSupportedCalled = services;
}
/**
* Gets the ServicesSupportedCalled parameter.
*
* @return the ServicesSupportedCalled parameter.
*/
public byte[] getServicesSupportedCalled() {
return servicesSupportedCalled;
}
/**
* Creates a server socket waiting on the configured port for incoming association requests.
*
* @param serverEventListener
* the listener that is notified of incoming writes and when the server stopped listening for new
* connections.
* @throws IOException
* if an error occurs binding to the port.
*/
public void startListening(ServerEventListener serverEventListener) throws IOException {
timer = new Timer();
if (serverSocketFactory == null) {
serverSocketFactory = ServerSocketFactory.getDefault();
}
acseSap = new ServerAcseSap(port, backlog, bindAddr, new AcseListener(this), serverSocketFactory);
acseSap.serverTSap.setMaxConnections(maxAssociations);
this.serverEventListener = serverEventListener;
listening = true;
acseSap.startListening();
}
/**
* Stops listening for new connections and closes all existing connections/associations.
*/
public void stop() {
acseSap.stopListening();
synchronized (associations) {
listening = false;
for (ServerAssociation association : associations) {
association.close();
}
associations.clear();
}
timer.cancel();
timer.purge();
}
void connectionIndication(AcseAssociation acseAssociation, ByteBuffer psdu) {
ServerAssociation association;
synchronized (associations) {
if (listening) {
association = new ServerAssociation(this);
associations.add(association);
}
else {
acseAssociation.close();
return;
}
}
try {
association.handleNewAssociation(acseAssociation, psdu);
} catch (Exception e) {
// Association closed because of an unexpected exception.
}
association.close();
synchronized (associations) {
associations.remove(association);
}
}
void serverStoppedListeningIndication(IOException e) {
if (serverEventListener != null) {
serverEventListener.serverStoppedListening(this);
}
}
public ServerModel getModelCopy() {
return serverModel.copy();
}
public void setValues(List bdas) {
synchronized (serverModel) {
for (BasicDataAttribute bda : bdas) {
// if (bda.getFunctionalConstraint() != FunctionalConstraint.ST) {
// logger.debug("fc:" + bda.getFunctionalConstraint());
// throw new IllegalArgumentException(
// "One can only set values of BDAs with Functional Constraint ST(status)");
// }
BasicDataAttribute bdaMirror = bda.mirror;
if (bdaMirror.dchg && bdaMirror.chgRcbs.size() != 0 && !bda.equals(bdaMirror)) {
bdaMirror.setValueFrom(bda);
synchronized (bdaMirror.chgRcbs) {
for (Urcb urcb : bdaMirror.chgRcbs) {
if (bdaMirror.dupd && urcb.getTrgOps().isDataUpdate()) {
urcb.report(bdaMirror, true, false, true);
}
else {
urcb.report(bdaMirror, true, false, false);
}
}
}
}
else if (bdaMirror.dupd && bdaMirror.dupdRcbs.size() != 0) {
bdaMirror.setValueFrom(bda);
synchronized (bdaMirror.dupdRcbs) {
for (Urcb urcb : bdaMirror.dupdRcbs) {
urcb.report(bdaMirror, false, false, true);
}
}
}
else if (bdaMirror.qchg && bdaMirror.chgRcbs.size() != 0 && !bda.equals(bdaMirror)) {
bdaMirror.setValueFrom(bda);
synchronized (bdaMirror.chgRcbs) {
for (Urcb urcb : bdaMirror.chgRcbs) {
urcb.report(bdaMirror, false, true, false);
}
}
}
else {
bdaMirror.setValueFrom(bda);
}
}
}
}
}