/* * 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); } } } } }