From 246b8b5f06b5144917a648a3cd329d234d46b02d Mon Sep 17 00:00:00 2001 From: mpeterss Date: Thu, 19 Dec 2019 10:23:51 +0100 Subject: [PATCH] Ported OCS gw code from Java to Kotlin --- .../org/ostelco/ocsgw/OcsApplication.java | 167 ------ .../ProtobufToDiameterConverter.java | 176 ------ .../datasource/local/LocalDataSource.java | 102 ---- .../datasource/protobuf/GrpcDataSource.java | 282 ---------- .../datasource/proxy/ProxyDataSource.java | 74 --- .../org/ostelco/ocsgw/utils/AppConfig.java | 96 ---- ocsgw/src/main/kotlin/OcsApplication.kt | 138 +++++ .../org/ostelco/ocsgw => kotlin}/OcsServer.kt | 17 +- .../converter/ProtobufToDiameterConverter.kt | 158 ++++++ .../ostelco/ocsgw/datasource/DataSource.kt} | 18 +- .../ocsgw/datasource/DataSourceTypes.kt | 0 .../ocsgw/datasource/local/LocalDataSource.kt | 90 +++ .../datasource/protobuf/GrpcDataSource.kt | 219 ++++++++ .../datasource/protobuf/ProtobufDataSource.kt | 2 +- .../datasource/protobuf/PubSubDataSource.kt | 0 .../ocsgw/datasource/proxy/ProxyDataSource.kt | 55 ++ .../org/ostelco/ocsgw/utils/AppConfig.kt | 92 +++ .../org/ostelco/ocsgw/OcsApplicationTest.java | 528 ------------------ .../java/org/ostelco/ocsgw/OcsHATest.java | 319 ----------- .../org/ostelco/ocsgw/OcsApplicationTest.kt | 439 +++++++++++++++ .../kotlin/org/ostelco/ocsgw/OcsHATest.kt | 264 +++++++++ 21 files changed, 1469 insertions(+), 1767 deletions(-) delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.java delete mode 100644 ocsgw/src/main/java/org/ostelco/ocsgw/utils/AppConfig.java create mode 100644 ocsgw/src/main/kotlin/OcsApplication.kt rename ocsgw/src/main/{java/org/ostelco/ocsgw => kotlin}/OcsServer.kt (92%) create mode 100644 ocsgw/src/main/kotlin/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.kt rename ocsgw/src/main/{java/org/ostelco/ocsgw/datasource/DataSource.java => kotlin/org/ostelco/ocsgw/datasource/DataSource.kt} (61%) rename ocsgw/src/main/{java => kotlin}/org/ostelco/ocsgw/datasource/DataSourceTypes.kt (100%) create mode 100644 ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/local/LocalDataSource.kt create mode 100644 ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.kt rename ocsgw/src/main/{java => kotlin}/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt (99%) rename ocsgw/src/main/{java => kotlin}/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt (100%) create mode 100644 ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.kt create mode 100644 ocsgw/src/main/kotlin/org/ostelco/ocsgw/utils/AppConfig.kt delete mode 100644 ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java delete mode 100644 ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java create mode 100644 ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsApplicationTest.kt create mode 100644 ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsHATest.kt diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java b/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java deleted file mode 100644 index e8bb63c64..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.ostelco.ocsgw; - -import com.google.cloud.storage.*; -import org.jdiameter.api.*; -import org.jdiameter.api.cca.ServerCCASession; -import org.jdiameter.api.cca.events.JCreditControlRequest; -import org.jdiameter.client.api.ISessionFactory; -import org.jdiameter.common.impl.app.cca.CCASessionFactoryImpl; -import org.jdiameter.server.impl.StackImpl; -import org.jdiameter.server.impl.app.cca.ServerCCASessionImpl; -import org.jdiameter.server.impl.helpers.XMLConfiguration; -import org.ostelco.diameter.model.RequestType; -import org.ostelco.ocsgw.utils.AppConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -public class OcsApplication extends CCASessionFactoryImpl implements NetworkReqListener { - - private static final Logger LOG = LoggerFactory.getLogger(OcsApplication.class); - private static final String DIAMETER_CONFIG_FILE = "server-jdiameter-config.xml"; - private static final String CONFIG_FOLDER = "/config/"; - private static final long APPLICATION_ID = 4L; // Diameter Credit Control Application (4) - private static final long VENDOR_ID_3GPP = 10415; - private static Stack stack = null; - - public static void main(String[] args) { - - Runtime.getRuntime().addShutdownHook(new Thread(OcsApplication::shutdown)); - - OcsApplication app = new OcsApplication(); - - String configFile = System.getenv("DIAMETER_CONFIG_FILE"); - if (configFile == null) { - configFile = DIAMETER_CONFIG_FILE; - } - - String configFolder = System.getenv("CONFIG_FOLDER"); - if (configFolder == null) { - configFolder = CONFIG_FOLDER; - } - - app.start(configFolder, configFile); - } - - - public static void shutdown() { - LOG.info("Shutting down OcsApplication..."); - if (stack != null) { - try { - stack.stop(30000, TimeUnit.MILLISECONDS , DisconnectCause.REBOOTING); - } catch (IllegalDiameterStateException | InternalException e) { - LOG.error("Failed to gracefully shutdown OcsApplication", e); - } - stack.destroy(); - } - } - - private void fetchConfig(final String configDir, final String configFile) { - final String vpcEnv = System.getenv("VPC_ENV"); - final String instance = System.getenv("INSTANCE"); - final String serviceFile = System.getenv("SERVICE_FILE"); - - if ((vpcEnv != null) && (instance != null)) { - String bucketName = "ocsgw-" + vpcEnv + "-" + instance + "-bucket"; - - fetchFromStorage(configFile, configDir, bucketName); - fetchFromStorage(serviceFile, configDir, bucketName); - } - } - - private void fetchFromStorage(String fileName, String configDir, String bucketName) { - if (fileName == null) { - return; - } - - LOG.debug("Downloading file : " + fileName); - - Storage storage = StorageOptions.getDefaultInstance().getService(); - Blob blobFile = storage.get(BlobId.of(bucketName, fileName)); - final Path destFilePath = Paths.get(configDir + "/" + fileName); - blobFile.downloadTo(destFilePath); - } - - - public void start(final String configDir, final String configFile) { - try { - - fetchConfig(configDir, configFile); - - Configuration diameterConfig = new XMLConfiguration(configDir + configFile); - stack = new StackImpl(); - sessionFactory = (ISessionFactory) stack.init(diameterConfig); - - OcsServer.INSTANCE.init$ocsgw(stack, new AppConfig()); - - Network network = stack.unwrap(Network.class); - network.addNetworkReqListener(this, ApplicationId.createByAuthAppId(0L, APPLICATION_ID)); - network.addNetworkReqListener(this, ApplicationId.createByAuthAppId(VENDOR_ID_3GPP, APPLICATION_ID)); - - stack.start(Mode.ALL_PEERS, 30000, TimeUnit.MILLISECONDS); - - init(sessionFactory); - sessionFactory.registerAppFacory(ServerCCASession.class, this); - printAppIds(); - - } catch (Exception e) { - LOG.error("Failure initializing OcsApplication", e); - } - } - - @Override - public Answer processRequest(Request request) { - LOG.debug("[<<] Received Request [{}]", request.getSessionId()); - try { - ServerCCASessionImpl session = sessionFactory.getNewAppSession(request.getSessionId(), ApplicationId.createByAuthAppId(4L), ServerCCASession.class); - session.processRequest(request); - LOG.debug("processRequest finished [{}]", request.getSessionId()); - } - catch (InternalException e) { - LOG.error("[><] Failure handling received request.", e); - } - - return null; - } - - @Override - public void doCreditControlRequest(ServerCCASession session, JCreditControlRequest request) { - - switch (request.getRequestTypeAVPValue()) { - case RequestType.INITIAL_REQUEST: - case RequestType.UPDATE_REQUEST: - case RequestType.TERMINATION_REQUEST: - LOG.info("[<<] Received Credit-Control-Request from P-GW [ {} ] [{}]", RequestType.getTypeAsString(request.getRequestTypeAVPValue()), session.getSessionId()); - try { - OcsServer.INSTANCE.handleRequest$ocsgw(session, request); - } catch (Exception e) { - LOG.error("[><] Failure processing Credit-Control-Request [" + RequestType.getTypeAsString(request.getRequestTypeAVPValue()) + "] + [session.getSessionId()]", e); - } - break; - case RequestType.EVENT_REQUEST: - LOG.info("[<<] Received Credit-Control-Request [EVENT]"); - break; - default: - break; - } - } - - - private void printAppIds() { - Set appIds = stack.getMetaData().getLocalPeer().getCommonApplications(); - - LOG.info("Diameter Stack :: Supporting {} applications.", appIds.size()); - for (ApplicationId id : appIds) { - LOG.info("Diameter Stack :: Common :: {}", id); - } - - LOG.info("Uri : " + stack.getMetaData().getLocalPeer().getUri()); - LOG.info("Realm : " + stack.getMetaData().getLocalPeer().getRealmName()); - LOG.info("IP : " + Arrays.toString(stack.getMetaData().getLocalPeer().getIPAddresses())); - } -} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java b/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java deleted file mode 100644 index 43fe8d5b5..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java +++ /dev/null @@ -1,176 +0,0 @@ -package org.ostelco.ocsgw.converter; - -import com.google.protobuf.ByteString; -import org.ostelco.diameter.CreditControlContext; -import org.ostelco.diameter.model.*; -import org.ostelco.ocs.api.CreditControlRequestInfo; -import org.ostelco.ocs.api.CreditControlRequestType; -import org.ostelco.ocs.api.PdpType; -import org.ostelco.ocs.api.PsInformation; -import org.ostelco.ocs.api.ServiceInfo; -import org.ostelco.ocsgw.datasource.protobuf.GrpcDataSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; -import java.util.Collections; - -import static org.ostelco.diameter.model.RequestType.*; - -public class ProtobufToDiameterConverter { - - private static final Logger LOG = LoggerFactory.getLogger(GrpcDataSource.class); - - /** - * Convert MultipleServiceCreditControl in gRPC format to diameter format - */ - public static MultipleServiceCreditControl convertMSCC(org.ostelco.ocs.api.MultipleServiceCreditControl msccGRPC) { - return new MultipleServiceCreditControl( - msccGRPC.getRatingGroup(), - (int) msccGRPC.getServiceIdentifier(), - Collections.singletonList(new ServiceUnit()), - Collections.singletonList(new ServiceUnit()), - new ServiceUnit(msccGRPC.getGranted().getTotalOctets(), 0, 0), - msccGRPC.getValidityTime(), - msccGRPC.getQuotaHoldingTime(), - msccGRPC.getVolumeQuotaThreshold(), - convertFinalUnitIndication(msccGRPC.getFinalUnitIndication()), - convertResultCode(msccGRPC.getResultCode())); - } - - /** - * Convert Diameter request type to gRPC - */ - public static CreditControlRequestType getRequestType(CreditControlContext context) { - switch (context.getOriginalCreditControlRequest().getRequestTypeAVPValue()) { - case INITIAL_REQUEST: - return CreditControlRequestType.INITIAL_REQUEST; - case UPDATE_REQUEST: - return CreditControlRequestType.UPDATE_REQUEST; - case TERMINATION_REQUEST: - return CreditControlRequestType.TERMINATION_REQUEST; - case EVENT_REQUEST: - return CreditControlRequestType.EVENT_REQUEST; - default: - LOG.warn("Unknown request type"); - return CreditControlRequestType.NONE; - } - } - - private static FinalUnitIndication convertFinalUnitIndication(org.ostelco.ocs.api.FinalUnitIndication fuiGrpc) { - if (!fuiGrpc.getIsSet()) { - return null; - } - return new FinalUnitIndication( - FinalUnitAction.values()[fuiGrpc.getFinalUnitAction().getNumber()], - fuiGrpc.getRestrictionFilterRuleList(), - fuiGrpc.getFilterIdList(), - new RedirectServer( - RedirectAddressType.values()[fuiGrpc.getRedirectServer().getRedirectAddressType().getNumber()], - fuiGrpc.getRedirectServer().getRedirectServerAddress() - ) - ); - } - - // We match the error codes on names in protobuf and internal model - public static ResultCode convertResultCode(org.ostelco.ocs.api.ResultCode resultCode) { - return ResultCode.valueOf(resultCode.name()); - } - - public static CreditControlRequestInfo convertRequestToProtobuf(final CreditControlContext context, @Nullable final String topicId) { - - try { - CreditControlRequestInfo.Builder builder = CreditControlRequestInfo - .newBuilder() - .setType(getRequestType(context)); - - if (topicId != null) { - builder.setTopicId(topicId); - } - - builder.setRequestNumber(context.getCreditControlRequest().getCcRequestNumber().getInteger32()); - - addMultipleServiceCreditControl(context, builder); - - builder.setRequestId(context.getSessionId()) - .setMsisdn(context.getCreditControlRequest().getMsisdn()) - .setImsi(context.getCreditControlRequest().getImsi()); - - addPsInformation(context, builder); - - return builder.build(); - - } catch (Exception e) { - LOG.error("Failed to create CreditControlRequestInfo [{}] [{}]", context.getCreditControlRequest().getMsisdn(), context.getSessionId(), e); - } - return null; - } - - private static void addMultipleServiceCreditControl(final CreditControlContext context, CreditControlRequestInfo.Builder builder) { - for (MultipleServiceCreditControl mscc : context.getCreditControlRequest().getMultipleServiceCreditControls()) { - - org.ostelco.ocs.api.MultipleServiceCreditControl.Builder protoMscc = org.ostelco.ocs.api.MultipleServiceCreditControl.newBuilder(); - - if (!mscc.getRequested().isEmpty()) { - - ServiceUnit requested = mscc.getRequested().get(0); - - protoMscc.setRequested(org.ostelco.ocs.api.ServiceUnit.newBuilder() - .setTotalOctets(requested.getTotal()) // fails at 55904 - .setInputOctets(0L) - .setOutputOctets(0L)); - } - - for (ServiceUnit used : mscc.getUsed()) { - - // We do not track CC-Service-Specific-Units or CC-Time - if (used.getTotal() > 0) { - protoMscc.setUsed(org.ostelco.ocs.api.ServiceUnit.newBuilder() - .setInputOctets(used.getInput()) - .setOutputOctets(used.getOutput()) - .setTotalOctets(used.getTotal())); - } - } - - protoMscc.setRatingGroup(mscc.getRatingGroup()); - protoMscc.setServiceIdentifier(mscc.getServiceIdentifier()); - - if (mscc.getReportingReason() != null) { - protoMscc.setReportingReasonValue(mscc.getReportingReason().ordinal()); - } else { - protoMscc.setReportingReasonValue(org.ostelco.ocs.api.ReportingReason.UNRECOGNIZED.ordinal()); - } - builder.addMscc(protoMscc); - } - } - - private static void addPsInformation(final CreditControlContext context, CreditControlRequestInfo.Builder builder) { - if (!context.getCreditControlRequest().getServiceInformation().isEmpty()) { - final org.ostelco.diameter.model.PsInformation psInformation - = context.getCreditControlRequest().getServiceInformation().get(0).getPsInformation().get(0); - - if (psInformation != null) { - PsInformation.Builder psInformationBuilder = org.ostelco.ocs.api.PsInformation.newBuilder(); - if (psInformation.getCalledStationId() != null) { - psInformationBuilder.setCalledStationId(psInformation.getCalledStationId()); - } - if (psInformation.getSgsnMccMnc() != null) { - psInformationBuilder.setSgsnMccMnc(psInformation.getSgsnMccMnc()); - } - if (psInformation.getImsiMccMnc() != null) { - psInformationBuilder.setImsiMccMnc(psInformation.getImsiMccMnc()); - } - if (psInformation.getUserLocationInfo() != null) { - psInformationBuilder.setUserLocationInfo(ByteString.copyFrom(psInformation.getUserLocationInfo())); - } - if (psInformation.getPdpType() != null) { - psInformationBuilder.setPdpType(PdpType.forNumber(psInformation.getPdpType())); - } - if (psInformation.getPdpAddress() != null) { - psInformationBuilder.setPdpAddress(ByteString.copyFrom(psInformation.getPdpAddress().getAddress())); - } - builder.setServiceInformation(ServiceInfo.newBuilder().setPsInformation(psInformationBuilder)); - } - } - } -} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java deleted file mode 100644 index 13a11f738..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.ostelco.ocsgw.datasource.local; - -import org.jdiameter.api.IllegalDiameterStateException; -import org.jdiameter.api.InternalException; -import org.jdiameter.api.OverloadException; -import org.jdiameter.api.RouteException; -import org.jdiameter.api.cca.ServerCCASession; -import org.ostelco.diameter.CreditControlContext; -import org.ostelco.diameter.model.*; -import org.ostelco.ocs.api.CreditControlRequestType; -import org.ostelco.ocsgw.OcsServer; -import org.ostelco.ocsgw.datasource.DataSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Local DataSource that will accept all Credit Control Requests - * Can be used as a bypass. - * - */ -public class LocalDataSource implements DataSource { - - private static final Logger LOG = LoggerFactory.getLogger(LocalDataSource.class); - - @Override - public void init() { - // No init needed - } - - @Override - public void handleRequest(CreditControlContext context) { - CreditControlAnswer answer = createCreditControlAnswer(context); - LOG.info("Got Credit-Control-Request [{}] [{}]", context.getCreditControlRequest().getMsisdn(), context.getSessionId()); - try { - final ServerCCASession session = OcsServer.INSTANCE.getStack().getSession(context.getSessionId(), ServerCCASession.class); - session.sendCreditControlAnswer(context.createCCA(answer)); - LOG.info("Sent Credit-Control-Answer [{}] [{}]", context.getCreditControlRequest().getMsisdn(), context.getSessionId()); - } catch (InternalException | IllegalDiameterStateException | RouteException | OverloadException | NullPointerException e) { - LOG.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.getCreditControlRequest().getMsisdn(), context.getSessionId(), e); - } - } - - private CreditControlAnswer createCreditControlAnswer(CreditControlContext context) { - - final List origMultipleServiceCreditControls = context.getCreditControlRequest().getMultipleServiceCreditControls(); - final List newMultipleServiceCreditControls = new ArrayList<>(); - - for (MultipleServiceCreditControl mscc : origMultipleServiceCreditControls) { - - FinalUnitIndication finalUnitIndication = null; - - if (context.getOriginalCreditControlRequest().getRequestTypeAVPValue() == CreditControlRequestType.TERMINATION_REQUEST.getNumber()) { - finalUnitIndication = new FinalUnitIndication( - FinalUnitAction.TERMINATE, - new ArrayList<>(), - new ArrayList<>(), - new RedirectServer( - RedirectAddressType.IPV4_ADDRESS, - "") - ); - } - - final List newRequested = new ArrayList<>(); - for (ServiceUnit requested : mscc.getRequested()) { - newRequested.add(new ServiceUnit(requested.getTotal(), 0, 0)); - } - - if (!newRequested.isEmpty()) { - final ServiceUnit granted = newRequested.get(0); - MultipleServiceCreditControl newMscc = new MultipleServiceCreditControl( - mscc.getRatingGroup(), - mscc.getServiceIdentifier(), - newRequested, - mscc.getUsed(), - granted, - mscc.getValidityTime(), - 7200, - (long) (granted.getTotal() * 0.2), // 20% - finalUnitIndication, - ResultCode.DIAMETER_SUCCESS); - - newMultipleServiceCreditControls.add(newMscc); - - } - } - - int validityTime = 0; - if (newMultipleServiceCreditControls.isEmpty()) { - validityTime = 86400; - } - - return new CreditControlAnswer(ResultCode.DIAMETER_SUCCESS, newMultipleServiceCreditControls, validityTime); - } - - public boolean isBlocked(final String msisdn) { - return false; - } -} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java deleted file mode 100644 index 5e9b2aa0e..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java +++ /dev/null @@ -1,282 +0,0 @@ -package org.ostelco.ocsgw.datasource.protobuf; - -import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; -import io.grpc.auth.MoreCallCredentials; -import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; -import io.grpc.stub.StreamObserver; -import org.ostelco.diameter.CreditControlContext; -import org.ostelco.ocs.api.ActivateRequest; -import org.ostelco.ocs.api.ActivateResponse; -import org.ostelco.ocs.api.CreditControlAnswerInfo; -import org.ostelco.ocs.api.CreditControlRequestInfo; -import org.ostelco.ocs.api.CreditControlRequestType; -import org.ostelco.ocs.api.OcsServiceGrpc; -import org.ostelco.ocsgw.datasource.DataSource; -import org.ostelco.ocsgw.utils.EventConsumer; -import org.ostelco.ocsgw.utils.EventProducer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.ssl.SSLException; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - - -/** - * Uses gRPC to fetch data remotely - */ -public class GrpcDataSource implements DataSource { - - private static final Logger LOG = LoggerFactory.getLogger(GrpcDataSource.class); - - private OcsServiceGrpc.OcsServiceStub ocsServiceStub; - - private StreamObserver creditControlRequestStream; - - private String ocsServerHostname; - - private ManagedChannel grpcChannel; - - private ServiceAccountJwtAccessCredentials jwtAccessCredentials; - - private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - - private ScheduledFuture reconnectStreamFuture = null; - - private final ConcurrentLinkedQueue requestQueue = new ConcurrentLinkedQueue<>(); - - private final EventProducer producer; - - private Thread consumerThread; - - private ProtobufDataSource protobufDataSource; - - /** - * Generate a new instance that connects to an endpoint, and - * optionally also encrypts the connection. - * - * @param ocsServerHostname The gRPC endpoint to connect the client to. - * @throws IOException - */ - public GrpcDataSource( - final ProtobufDataSource protobufDataSource, - final String ocsServerHostname) throws IOException { - - this.protobufDataSource = protobufDataSource; - - this.ocsServerHostname = ocsServerHostname; - - LOG.info("Created GrpcDataSource"); - LOG.info("ocsServerHostname : {}", ocsServerHostname); - - // Not using the standard GOOGLE_APPLICATION_CREDENTIALS for this - // as we need to download the file using container credentials in - // OcsApplication. - final String serviceAccountFile = "/config/" + System.getenv("SERVICE_FILE"); - jwtAccessCredentials = ServiceAccountJwtAccessCredentials.fromStream(new FileInputStream(serviceAccountFile)); - - producer = new EventProducer<>(requestQueue); - } - - @Override - public void init() { - - setupChannel(); - initCreditControlRequestStream(); - initActivateStream(); - initKeepAlive(); - - setupEventConsumer(); - } - - private void setupEventConsumer() { - - // ToDo : Is this enough to know the thread stopped? - if (consumerThread != null) { - consumerThread.interrupt(); - } - - EventConsumer requestInfoConsumer = new EventConsumer<>(requestQueue, creditControlRequestStream); - consumerThread = new Thread(requestInfoConsumer); - consumerThread.start(); - } - - private void setupChannel() { - - - ManagedChannelBuilder channelBuilder; - - // Set up a channel to be used to communicate as an OCS instance, - // to a gRPC instance. - - final boolean disableTls = Boolean.valueOf(System.getenv("DISABLE_TLS")); - - try { - if (disableTls) { - channelBuilder = ManagedChannelBuilder - .forTarget(ocsServerHostname) - .usePlaintext(); - } else { - final NettyChannelBuilder nettyChannelBuilder = NettyChannelBuilder - .forTarget(ocsServerHostname); - - - channelBuilder = Files.exists(Paths.get("/cert/ocs.crt")) - ? nettyChannelBuilder.sslContext( - GrpcSslContexts.forClient().trustManager(new File("/cert/ocs.crt")).build()) - .useTransportSecurity() - : nettyChannelBuilder.useTransportSecurity(); - } - - if (grpcChannel != null) { - - grpcChannel.shutdownNow(); - try { - boolean isShutdown = grpcChannel.awaitTermination(3, TimeUnit.SECONDS); - LOG.info("grpcChannel is shutdown : " + isShutdown); - } catch (InterruptedException e) { - LOG.info("Error shutting down gRPC channel"); - } - } - - grpcChannel = channelBuilder - .keepAliveWithoutCalls(true) -// .keepAliveTimeout(1, TimeUnit.MINUTES) -// .keepAliveTime(20, TimeUnit.MINUTES) - .build(); - - ocsServiceStub = OcsServiceGrpc.newStub(grpcChannel) - .withCallCredentials(MoreCallCredentials.from(jwtAccessCredentials)); - - } catch (SSLException e) { - LOG.warn("Failed to setup gRPC channel", e); - } - } - - - /** - * Init the gRPC channel that will be used to send/receive - * diameter messages to the OCS module in Prime. - */ - private void initCreditControlRequestStream() { - creditControlRequestStream = ocsServiceStub.creditControlRequest( - new StreamObserver() { - public void onNext(CreditControlAnswerInfo answer) { - protobufDataSource.handleCcrAnswer(answer); - } - - @Override - public void onError(Throwable t) { - LOG.error("CreditControlRequestStream error", t); - if (t instanceof StatusRuntimeException) { - reconnectStreams(); - } - } - - @Override - public void onCompleted() { - // Nothing to do here - } - }); - } - - /** - * Init the gRPC channel that will be used to get activation requests from the - * OCS. These requests are send when we need to reactivate a diameter session. For - * example on a topup event. - */ - private void initActivateStream() { - ActivateRequest dummyActivate = ActivateRequest.newBuilder().build(); - ocsServiceStub.activate(dummyActivate, new StreamObserver() { - @Override - public void onNext(ActivateResponse activateResponse) { - protobufDataSource.handleActivateResponse(activateResponse); - } - - @Override - public void onError(Throwable t) { - LOG.error("ActivateObserver error", t); - if (t instanceof StatusRuntimeException) { - reconnectStreams(); - } - } - - @Override - public void onCompleted() { - // Nothing to do here - } - }); - } - - /** - * The keep alive messages are sent on the creditControlRequestStream - * to force it to stay open avoiding reconnects on the gRPC channel. - */ - private void initKeepAlive() { - // this is used to keep connection alive - executorService.scheduleWithFixedDelay(() -> { - final CreditControlRequestInfo ccr = CreditControlRequestInfo.newBuilder() - .setType(CreditControlRequestType.NONE) - .build(); - producer.queueEvent(ccr); - }, - 10, - 5, - TimeUnit.SECONDS); - } - - - private void reconnectStreams() { - LOG.debug("reconnectStreams called"); - - if (!isReconnecting()) { - - reconnectStreamFuture = executorService.schedule((Callable) () -> { - LOG.debug("Reconnecting gRPC streams"); - setupChannel(); - initCreditControlRequestStream(); - setupEventConsumer(); - initActivateStream(); - return "Called!"; - }, - 5, - TimeUnit.SECONDS); - } - } - - private boolean isReconnecting() { - if (reconnectStreamFuture != null) { - return !reconnectStreamFuture.isDone(); - } - return false; - } - - @Override - public void handleRequest(final CreditControlContext context) { - - CreditControlRequestInfo creditControlRequestInfo = protobufDataSource.handleRequest(context, null); - - if (creditControlRequestInfo != null) { - context.setSentToOcsTime(System.currentTimeMillis()); - producer.queueEvent(creditControlRequestInfo); - } - } - - @Override - public boolean isBlocked(final String msisdn) { - return protobufDataSource.isBlocked(msisdn); - } -} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.java deleted file mode 100644 index 286b02749..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.ostelco.ocsgw.datasource.proxy; - -import org.ostelco.ocsgw.datasource.DataSource; -import org.ostelco.ocsgw.datasource.local.LocalDataSource; -import org.ostelco.diameter.CreditControlContext; -import org.ostelco.ocs.api.CreditControlRequestType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Proxy DataSource is a combination of the Local DataSource and any other - * DataSource. - * - * With the proxy approach the CCR-I request will not use the LocalDataStore. If the - * reply for the CCR-I is accepted the following CCR-U requests will use the LocalDataSource - * to quickly reply and accept the query. But it will also send the same request on using the - * secondary DataSource. When the secondary DataSource deny a CCR-U then the next request will be denied. - * - * The DataSource will keep a block list of msisdns that has failed to query new buckets when doing CCR. - */ -public class ProxyDataSource implements DataSource { - - private final DataSource local = new LocalDataSource(); - - private final DataSource secondary; - - private static final Logger LOG = LoggerFactory.getLogger(ProxyDataSource.class); - - public ProxyDataSource(DataSource dataSource) { - secondary = dataSource; - } - - @Override - public void init() { - // No init needed - } - - @Override - public void handleRequest(CreditControlContext context) { - - switch (context.getOriginalCreditControlRequest().getRequestTypeAVPValue()) { - case CreditControlRequestType.INITIAL_REQUEST_VALUE: - secondary.handleRequest(context); - break; - case CreditControlRequestType.UPDATE_REQUEST_VALUE: - if (!secondary.isBlocked(context.getCreditControlRequest().getMsisdn())) { - proxyAnswer(context); - } else { - secondary.handleRequest(context); - } - break; - case CreditControlRequestType.TERMINATION_REQUEST_VALUE: - proxyAnswer(context); - break; - default: - LOG.warn("Unknown request type : {}", context.getOriginalCreditControlRequest().getRequestTypeAVPValue()); - } - } - - /** - * Use the local data source to send an answer directly to P-GW. - * Use secondary to report the usage to OCS. - */ - private void proxyAnswer(CreditControlContext context) { - local.handleRequest(context); - context.setSkipAnswer(true); - secondary.handleRequest(context); - } - - @Override - public boolean isBlocked(final String msisdn) { - return secondary.isBlocked(msisdn); - } -} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/utils/AppConfig.java b/ocsgw/src/main/java/org/ostelco/ocsgw/utils/AppConfig.java deleted file mode 100644 index d94d5b1ac..000000000 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/utils/AppConfig.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.ostelco.ocsgw.utils; - -import org.ostelco.ocsgw.datasource.DataSourceType; -import org.ostelco.ocsgw.datasource.SecondaryDataSourceType; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -public class AppConfig { - - private final Properties prop = new Properties(); - - public AppConfig() throws IOException { - final String fileName = "config.properties"; - InputStream iStream = this.getClass().getClassLoader().getResourceAsStream(fileName); - prop.load(iStream); - iStream.close(); - } - - public DataSourceType getDataStoreType () { - // OCS_DATASOURCE_TYPE env has higher preference over config.properties - final String dataSource = System.getenv("OCS_DATASOURCE_TYPE"); - if (dataSource == null || dataSource.isEmpty()) { - try { - return DataSourceType.valueOf(prop.getProperty("DataStoreType", "Local")); - } catch (IllegalArgumentException e) { - return DataSourceType.Local; - } - } - try { - return DataSourceType.valueOf(dataSource); - } catch (IllegalArgumentException e) { - return DataSourceType.Local; - } - } - - public Long getDefaultRequestedServiceUnit () { - final String defaultRequestedServiceUnit = System.getProperty("DEFAULT_REQUESTED_SERVICE_UNIT"); - if (defaultRequestedServiceUnit == null || defaultRequestedServiceUnit.isEmpty()) { - return 40_000_000L; - } else { - return Long.parseLong(defaultRequestedServiceUnit); - } - } - - public SecondaryDataSourceType getSecondaryDataStoreType () { - // OCS_SECONDARY_DATASOURCE_TYPE env has higher preference over config.properties - final String secondaryDataSource = System.getenv("OCS_SECONDARY_DATASOURCE_TYPE"); - if (secondaryDataSource == null || secondaryDataSource.isEmpty()) { - try { - return SecondaryDataSourceType.valueOf(prop.getProperty("SecondaryDataStoreType", "PubSub")); - } catch (IllegalArgumentException e) { - return SecondaryDataSourceType.PubSub; - } - } - try { - return SecondaryDataSourceType.valueOf(secondaryDataSource); - } catch (IllegalArgumentException e) { - return SecondaryDataSourceType.PubSub; - } - } - - public String getGrpcServer() { - return getEnvProperty("OCS_GRPC_SERVER"); - } - - public String getPubSubProjectId() { - return getEnvProperty("PUBSUB_PROJECT_ID"); - } - - public String getPubSubTopicIdForCcr() { - return getEnvProperty("PUBSUB_CCR_TOPIC_ID"); - } - - public String getPubSubTopicIdForCca() { - return getEnvProperty("PUBSUB_CCA_TOPIC_ID"); - } - - public String getPubSubSubscriptionIdForCca() { - return getEnvProperty("PUBSUB_CCA_SUBSCRIPTION_ID"); - } - - public String getPubSubSubscriptionIdForActivate() { - return getEnvProperty("PUBSUB_ACTIVATE_SUBSCRIPTION_ID"); - } - - - private String getEnvProperty(String propertyName) { - final String value = System.getenv(propertyName); - if (value == null || value.isEmpty()) { - throw new Error("No "+ propertyName + " set in env"); - } - return value; - } -} diff --git a/ocsgw/src/main/kotlin/OcsApplication.kt b/ocsgw/src/main/kotlin/OcsApplication.kt new file mode 100644 index 000000000..cf1b46f93 --- /dev/null +++ b/ocsgw/src/main/kotlin/OcsApplication.kt @@ -0,0 +1,138 @@ +package org.ostelco.ocsgw + +import OcsServer +import com.google.cloud.storage.BlobId +import com.google.cloud.storage.StorageOptions +import org.jdiameter.api.* +import org.jdiameter.api.Stack +import org.jdiameter.api.cca.ServerCCASession +import org.jdiameter.api.cca.events.JCreditControlRequest +import org.jdiameter.client.api.ISessionFactory +import org.jdiameter.common.impl.app.cca.CCASessionFactoryImpl +import org.jdiameter.server.impl.StackImpl +import org.jdiameter.server.impl.app.cca.ServerCCASessionImpl +import org.jdiameter.server.impl.helpers.XMLConfiguration +import org.ostelco.diameter.model.RequestType +import org.ostelco.diameter.model.RequestType.getTypeAsString +import org.ostelco.ocsgw.utils.AppConfig +import org.slf4j.LoggerFactory +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.TimeUnit + +class OcsApplication : CCASessionFactoryImpl(), NetworkReqListener { + private fun fetchConfig(configDir: String?, configFile: String?) { + val vpcEnv = System.getenv("VPC_ENV") + val instance = System.getenv("INSTANCE") + val serviceFile = System.getenv("SERVICE_FILE") + if (vpcEnv != null && instance != null) { + val bucketName = "ocsgw-$vpcEnv-$instance-bucket" + fetchFromStorage(configFile, configDir, bucketName) + fetchFromStorage(serviceFile, configDir, bucketName) + } + } + + private fun fetchFromStorage(fileName: String?, configDir: String?, bucketName: String) { + if (fileName == null) { + return + } + LOG.debug("Downloading file : $fileName") + val storage = StorageOptions.getDefaultInstance().service + val blobFile = storage[BlobId.of(bucketName, fileName)] + val destFilePath = Paths.get("$configDir/$fileName") + blobFile.downloadTo(destFilePath) + } + + fun start(configDir: String?, configFile: String?) { + try { + fetchConfig(configDir, configFile) + val diameterConfig: Configuration = XMLConfiguration(configDir + configFile) + stack = StackImpl() + sessionFactory = stack.init(diameterConfig) as ISessionFactory + OcsServer.init(stack, AppConfig()) + val network = stack.unwrap(Network::class.java) + network.addNetworkReqListener(this, ApplicationId.createByAuthAppId(0L, APPLICATION_ID)) + network.addNetworkReqListener(this, ApplicationId.createByAuthAppId(VENDOR_ID_3GPP, APPLICATION_ID)) + stack.start(Mode.ALL_PEERS, 30000, TimeUnit.MILLISECONDS) + init(sessionFactory) + sessionFactory.registerAppFacory(ServerCCASession::class.java, this) + printAppIds() + } catch (e: Exception) { + LOG.error("Failure initializing OcsApplication", e) + } + } + + override fun processRequest(request: Request): Answer? { + LOG.debug("[<<] Received Request [{}]", request.sessionId) + try { + val session = sessionFactory.getNewAppSession(request.sessionId, ApplicationId.createByAuthAppId(4L), ServerCCASession::class.java) + session.processRequest(request) + LOG.debug("processRequest finished [{}]", request.sessionId) + } catch (e: InternalException) { + LOG.error("[><] Failure handling received request.", e) + } + return null + } + + override fun doCreditControlRequest(session: ServerCCASession, request: JCreditControlRequest) { + when (request.requestTypeAVPValue) { + RequestType.INITIAL_REQUEST, RequestType.UPDATE_REQUEST, RequestType.TERMINATION_REQUEST -> { + LOG.info("[<<] Received Credit-Control-Request from P-GW [ {} ] [{}]", getTypeAsString(request.requestTypeAVPValue), session.sessionId) + try { + OcsServer.handleRequest(session, request) + } catch (e: Exception) { + LOG.error("[><] Failure processing Credit-Control-Request [" + getTypeAsString(request.requestTypeAVPValue) + "] + [session.getSessionId()]", e) + } + } + RequestType.EVENT_REQUEST -> LOG.info("[<<] Received Credit-Control-Request [EVENT]") + else -> { + } + } + } + + private fun printAppIds() { + val appIds = stack.metaData.localPeer.commonApplications + LOG.info("Diameter Stack :: Supporting {} applications.", appIds.size) + for (id in appIds) { + LOG.info("Diameter Stack :: Common :: {}", id) + } + LOG.info("Uri : " + stack.metaData.localPeer.uri) + LOG.info("Realm : " + stack.metaData.localPeer.realmName) + LOG.info("IP : " + Arrays.toString(stack.metaData.localPeer.ipAddresses)) + } + + companion object { + private val LOG = LoggerFactory.getLogger(OcsApplication::class.java) + private const val DIAMETER_CONFIG_FILE = "server-jdiameter-config.xml" + private const val CONFIG_FOLDER = "/config/" + private const val APPLICATION_ID = 4L // Diameter Credit Control Application (4) + private const val VENDOR_ID_3GPP: Long = 10415 + private lateinit var stack : Stack + @JvmStatic + fun main(args: Array) { + Runtime.getRuntime().addShutdownHook(Thread(Runnable { shutdown() })) + val app = OcsApplication() + var configFile = System.getenv("DIAMETER_CONFIG_FILE") + if (configFile == null) { + configFile = DIAMETER_CONFIG_FILE + } + var configFolder = System.getenv("CONFIG_FOLDER") + if (configFolder == null) { + configFolder = CONFIG_FOLDER + } + app.start(configFolder, configFile) + } + + fun shutdown() { + LOG.info("Shutting down OcsApplication...") + try { + stack.stop(30000, TimeUnit.MILLISECONDS, DisconnectCause.REBOOTING) + } catch (e: IllegalDiameterStateException) { + LOG.error("Failed to gracefully shutdown OcsApplication", e) + } catch (e: InternalException) { + LOG.error("Failed to gracefully shutdown OcsApplication", e) + } + stack.destroy() + } + } +} \ No newline at end of file diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsServer.kt b/ocsgw/src/main/kotlin/OcsServer.kt similarity index 92% rename from ocsgw/src/main/java/org/ostelco/ocsgw/OcsServer.kt rename to ocsgw/src/main/kotlin/OcsServer.kt index 5db93c017..97e62fb2c 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsServer.kt +++ b/ocsgw/src/main/kotlin/OcsServer.kt @@ -1,5 +1,3 @@ -package org.ostelco.ocsgw - import org.jdiameter.api.ApplicationId import org.jdiameter.api.Avp import org.jdiameter.api.IllegalDiameterStateException @@ -119,12 +117,12 @@ object OcsServer { } - internal fun init(stack: Stack, appConfig: AppConfig) { - this.stack = stack - this.localPeerFQDN = stack.metaData.localPeer.uri.fqdn - this.localPeerRealm = stack.metaData.localPeer.realmName + fun init(stack: Stack, appConfig: AppConfig) { + OcsServer.stack = stack + localPeerFQDN = stack.metaData.localPeer.uri.fqdn + localPeerRealm = stack.metaData.localPeer.realmName - this.defaultRequestedServiceUnit = appConfig.defaultRequestedServiceUnit + defaultRequestedServiceUnit = appConfig.defaultRequestedServiceUnit val protobufDataSource = ProtobufDataSource() @@ -134,7 +132,6 @@ object OcsServer { val secondary = when (appConfig.secondaryDataStoreType) { SecondaryDataSourceType.PubSub -> getPubSubDataSource(protobufDataSource, appConfig) SecondaryDataSourceType.gRPC -> getGrpcDataSource(protobufDataSource, appConfig) - else -> getPubSubDataSource(protobufDataSource, appConfig) } secondary.init() ProxyDataSource(secondary) @@ -151,10 +148,6 @@ object OcsServer { logger.info("Using GrpcDataSource") getGrpcDataSource(protobufDataSource, appConfig) } - else -> { - logger.warn("Unknown DataStoreType {}", appConfig.dataStoreType) - LocalDataSource() - } } source?.init() } diff --git a/ocsgw/src/main/kotlin/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.kt new file mode 100644 index 000000000..aecd3f59e --- /dev/null +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.kt @@ -0,0 +1,158 @@ +package org.ostelco.ocsgw.converter; + +import com.google.protobuf.ByteString +import org.ostelco.diameter.CreditControlContext +import org.ostelco.diameter.getLogger +import org.ostelco.diameter.model.FinalUnitAction +import org.ostelco.diameter.model.FinalUnitIndication +import org.ostelco.diameter.model.MultipleServiceCreditControl +import org.ostelco.diameter.model.RedirectAddressType +import org.ostelco.diameter.model.RedirectServer +import org.ostelco.diameter.model.ResultCode +import org.ostelco.diameter.model.ServiceUnit +import org.ostelco.diameter.model.RequestType +import org.ostelco.ocs.api.* +import org.ostelco.ocs.api.PsInformation +import org.ostelco.ocs.api.ReportingReason + + +class ProtobufToDiameterConverter { + + companion object { + + private val logger by getLogger() + + fun convertRequestToProtobuf(context: CreditControlContext, topicId: String?): CreditControlRequestInfo? { + try { + val builder = CreditControlRequestInfo + .newBuilder() + .setType(getRequestType(context)) + if (topicId != null) { + builder.topicId = topicId + } + builder.requestNumber = context.creditControlRequest.ccRequestNumber!!.integer32 + addMultipleServiceCreditControl(context, builder) + builder.setRequestId(context.sessionId) + .setMsisdn(context.creditControlRequest.msisdn) + .setImsi(context.creditControlRequest.imsi) + addPsInformation(context, builder) + return builder.build() + } catch (e: Exception) { + logger.error("Failed to create CreditControlRequestInfo [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } + return null + } + + /** + * Convert MultipleServiceCreditControl in protobuf format to diameter format + */ + fun convertMSCC(msccGRPC: org.ostelco.ocs.api.MultipleServiceCreditControl): MultipleServiceCreditControl { + return MultipleServiceCreditControl( + msccGRPC.ratingGroup, + msccGRPC.serviceIdentifier, + listOf(ServiceUnit()), + listOf(ServiceUnit()), + ServiceUnit(msccGRPC.granted.totalOctets, 0, 0), + msccGRPC.validityTime, + msccGRPC.quotaHoldingTime, + msccGRPC.volumeQuotaThreshold, + convertFinalUnitIndication(msccGRPC.finalUnitIndication), + convertResultCode(msccGRPC.resultCode)) + } + + // We match the error codes on names in protobuf and internal model + fun convertResultCode(resultCode: org.ostelco.ocs.api.ResultCode): ResultCode { + try { + return ResultCode.valueOf(resultCode.name) + } catch (e: IllegalArgumentException) { + logger.warn("Unknown ResultCode $resultCode") + } + return ResultCode.DIAMETER_UNABLE_TO_COMPLY + } + + /** + * Convert Diameter request type to protobuf + */ + fun getRequestType(context: CreditControlContext): CreditControlRequestType { + return when (context.originalCreditControlRequest.requestTypeAVPValue) { + RequestType.INITIAL_REQUEST -> CreditControlRequestType.INITIAL_REQUEST + RequestType.UPDATE_REQUEST -> CreditControlRequestType.UPDATE_REQUEST + RequestType.TERMINATION_REQUEST -> CreditControlRequestType.TERMINATION_REQUEST + RequestType.EVENT_REQUEST -> CreditControlRequestType.EVENT_REQUEST + else -> { + logger.warn("Unknown request type") + CreditControlRequestType.NONE + } + } + } + + private fun addPsInformation(context: CreditControlContext, builder: CreditControlRequestInfo.Builder) { + if (!context.creditControlRequest.serviceInformation.isEmpty()) { + val psInformation = context.creditControlRequest.serviceInformation[0].psInformation[0] + val psInformationBuilder = PsInformation.newBuilder() + if (psInformation.calledStationId != null) { + psInformationBuilder.calledStationId = psInformation.calledStationId + } + if (psInformation.sgsnMccMnc != null) { + psInformationBuilder.sgsnMccMnc = psInformation.sgsnMccMnc + } + if (psInformation.imsiMccMnc != null) { + psInformationBuilder.imsiMccMnc = psInformation.imsiMccMnc + } + + psInformation.userLocationInfo?.let { psInformationBuilder.userLocationInfo = ByteString.copyFrom(it) } + + psInformation.pdpType?.let { psInformationBuilder.pdpType = PdpType.forNumber(it) } + + psInformation.pdpAddress?.let { psInformationBuilder.pdpAddress = ByteString.copyFrom(it.address) } + + builder.setServiceInformation(ServiceInfo.newBuilder().setPsInformation(psInformationBuilder)) + } + } + + private fun addMultipleServiceCreditControl(context: CreditControlContext, builder: CreditControlRequestInfo.Builder) { + for (mscc in context.creditControlRequest.multipleServiceCreditControls) { + val msccBuilder = org.ostelco.ocs.api.MultipleServiceCreditControl.newBuilder() + if (!mscc.requested.isEmpty()) { + val requested = mscc.requested[0] + msccBuilder.setRequested(org.ostelco.ocs.api.ServiceUnit.newBuilder() + .setTotalOctets(requested.total) + .setInputOctets(0L) + .setOutputOctets(0L)) + } + for (used in mscc.used) { // We do not track CC-Service-Specific-Units or CC-Time + if (used.total > 0) { + msccBuilder.setUsed(org.ostelco.ocs.api.ServiceUnit.newBuilder() + .setInputOctets(used.input) + .setOutputOctets(used.output) + .setTotalOctets(used.total)) + } + } + msccBuilder.ratingGroup = mscc.ratingGroup + msccBuilder.serviceIdentifier = mscc.serviceIdentifier + + val reportingReason = mscc.reportingReason + if (reportingReason != null) { + msccBuilder.reportingReasonValue = reportingReason.ordinal + } else { + msccBuilder.reportingReasonValue = ReportingReason.UNRECOGNIZED.ordinal + } + builder.addMscc(msccBuilder) + } + } + + private fun convertFinalUnitIndication(fuiGrpc: org.ostelco.ocs.api.FinalUnitIndication): FinalUnitIndication? { + return if (!fuiGrpc.isSet) { + null + } else FinalUnitIndication( + FinalUnitAction.values()[fuiGrpc.finalUnitAction.number], + fuiGrpc.restrictionFilterRuleList, + fuiGrpc.filterIdList, + RedirectServer( + RedirectAddressType.values()[fuiGrpc.redirectServer.redirectAddressType.number], + fuiGrpc.redirectServer.redirectServerAddress + ) + ) + } + } +} diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/DataSource.java b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/DataSource.kt similarity index 61% rename from ocsgw/src/main/java/org/ostelco/ocsgw/datasource/DataSource.java rename to ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/DataSource.kt index 3e2bdc0f2..a3a3c0fe4 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/DataSource.java +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/DataSource.kt @@ -1,25 +1,23 @@ -package org.ostelco.ocsgw.datasource; +package org.ostelco.ocsgw.datasource - -import org.ostelco.diameter.CreditControlContext; +import org.ostelco.diameter.CreditControlContext /** * Interface to interact with a datasource. * */ -public interface DataSource { - +interface DataSource { /** - * Initiates the datasource + * Initiates the datasource */ - void init(); + fun init() /** * Forward a new initial/update/terminate request. * * @param context That holds the request and session */ - void handleRequest(CreditControlContext context); + fun handleRequest(context: CreditControlContext) /** * Check if a subscriber is on the blocked-list. @@ -28,5 +26,5 @@ public interface DataSource { * * @param msisdn Subscriber msisdn to check */ - boolean isBlocked(final String msisdn); -} + fun isBlocked(msisdn: String): Boolean +} \ No newline at end of file diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/DataSourceTypes.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/DataSourceTypes.kt similarity index 100% rename from ocsgw/src/main/java/org/ostelco/ocsgw/datasource/DataSourceTypes.kt rename to ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/DataSourceTypes.kt diff --git a/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/local/LocalDataSource.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/local/LocalDataSource.kt new file mode 100644 index 000000000..9d3aa9745 --- /dev/null +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/local/LocalDataSource.kt @@ -0,0 +1,90 @@ +package org.ostelco.ocsgw.datasource.local + +import org.jdiameter.api.IllegalDiameterStateException +import org.jdiameter.api.InternalException +import org.jdiameter.api.OverloadException +import org.jdiameter.api.RouteException +import org.jdiameter.api.cca.ServerCCASession +import org.ostelco.diameter.CreditControlContext +import org.ostelco.diameter.model.* +import org.ostelco.ocs.api.CreditControlRequestType +import OcsServer.stack +import org.ostelco.diameter.getLogger +import org.ostelco.ocsgw.datasource.DataSource +import java.util.* + +/** + * Local DataSource that will accept all Credit Control Requests + * Can be used as a bypass. + */ +class LocalDataSource : DataSource { + + private val logger by getLogger() + + override fun init() { // No init needed + } + + override fun handleRequest(context: CreditControlContext) { + val answer = createCreditControlAnswer(context) + logger.info("Got Credit-Control-Request [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId) + try { + val session = stack!!.getSession(context.sessionId, ServerCCASession::class.java) + session.sendCreditControlAnswer(context.createCCA(answer)) + logger.info("Sent Credit-Control-Answer [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId) + } catch (e: InternalException) { + logger.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } catch (e: IllegalDiameterStateException) { + logger.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } catch (e: RouteException) { + logger.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } catch (e: OverloadException) { + logger.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } catch (e: NullPointerException) { + logger.error("Failed to send Credit-Control-Answer. [{}] [{}]", context.creditControlRequest.msisdn, context.sessionId, e) + } + } + + override fun isBlocked(msisdn: String): Boolean { + return false + } + + private fun createCreditControlAnswer(context: CreditControlContext): CreditControlAnswer { + val origMultipleServiceCreditControls = context.creditControlRequest.multipleServiceCreditControls + val newMultipleServiceCreditControls: MutableList = ArrayList() + for (mscc in origMultipleServiceCreditControls) { + var finalUnitIndication: FinalUnitIndication? = null + if (context.originalCreditControlRequest.requestTypeAVPValue == CreditControlRequestType.TERMINATION_REQUEST.number) { + finalUnitIndication = FinalUnitIndication( + FinalUnitAction.TERMINATE, + ArrayList(), + ArrayList(), + RedirectServer(RedirectAddressType.IPV4_ADDRESS,"") + ) + } + val newRequested: MutableList = ArrayList() + for (requested in mscc.requested) { + newRequested.add(ServiceUnit(requested.total, 0, 0)) + } + if (!newRequested.isEmpty()) { + val granted = newRequested[0] + val newMscc = MultipleServiceCreditControl( + mscc.ratingGroup, + mscc.serviceIdentifier, + newRequested, + mscc.used, + granted, + mscc.validityTime, + 7200, + (granted.total * 0.2).toLong(), // 20% + finalUnitIndication, + ResultCode.DIAMETER_SUCCESS) + newMultipleServiceCreditControls.add(newMscc) + } + } + var validityTime = 0 + if (newMultipleServiceCreditControls.isEmpty()) { + validityTime = 86400 + } + return CreditControlAnswer(ResultCode.DIAMETER_SUCCESS, newMultipleServiceCreditControls, validityTime) + } +} \ No newline at end of file diff --git a/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.kt new file mode 100644 index 000000000..f178eda92 --- /dev/null +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.kt @@ -0,0 +1,219 @@ +package org.ostelco.ocsgw.datasource.protobuf + +import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.StatusRuntimeException +import io.grpc.auth.MoreCallCredentials +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder +import io.grpc.stub.StreamObserver +import org.ostelco.diameter.CreditControlContext +import org.ostelco.diameter.getLogger +import org.ostelco.ocs.api.* +import org.ostelco.ocs.api.OcsServiceGrpc.OcsServiceStub +import org.ostelco.ocsgw.datasource.DataSource +import org.ostelco.ocsgw.utils.EventConsumer +import org.ostelco.ocsgw.utils.EventProducer +import java.io.File +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.* +import javax.net.ssl.SSLException + +/** + * Uses gRPC to fetch data remotely + */ +class GrpcDataSource( + private val protobufDataSource: ProtobufDataSource, + private val ocsServerHostname: String) : DataSource { + + private var ocsServiceStub: OcsServiceStub? = null + private lateinit var creditControlRequestStream: StreamObserver + private var grpcChannel: ManagedChannel? = null + private val jwtAccessCredentials: ServiceAccountJwtAccessCredentials + private val executorService = Executors.newSingleThreadScheduledExecutor() + private var reconnectStreamFuture: ScheduledFuture<*>? = null + private val requestQueue = ConcurrentLinkedQueue() + private val producer: EventProducer + private var consumerThread: Thread? = null + private val logger by getLogger() + + override fun init() { + setupChannel() + initCreditControlRequestStream() + initActivateStream() + initKeepAlive() + setupEventConsumer() + } + + private fun setupEventConsumer() { + + consumerThread?.interrupt() + + val requestInfoConsumer = EventConsumer(requestQueue, creditControlRequestStream) + consumerThread = Thread(requestInfoConsumer) + consumerThread!!.start() + } + + private fun setupChannel() { + val channelBuilder: ManagedChannelBuilder<*> + // Set up a channel to be used to communicate as an OCS instance, + // to a gRPC instance. + val disableTls = java.lang.Boolean.valueOf(System.getenv("DISABLE_TLS")) + try { + channelBuilder = if (disableTls) { + ManagedChannelBuilder + .forTarget(ocsServerHostname) + .usePlaintext() + } else { + val nettyChannelBuilder = NettyChannelBuilder.forTarget(ocsServerHostname) + if (Files.exists(Paths.get("/cert/ocs.crt"))) + nettyChannelBuilder.sslContext( + GrpcSslContexts.forClient().trustManager(File("/cert/ocs.crt")).build()) + .useTransportSecurity() + else + nettyChannelBuilder.useTransportSecurity() + } + + grpcChannel?.let{shutdownGprsChannel(it)} + + grpcChannel = channelBuilder + .keepAliveWithoutCalls(true) + .build() + ocsServiceStub = OcsServiceGrpc.newStub(grpcChannel) + .withCallCredentials(MoreCallCredentials.from(jwtAccessCredentials)) + } catch (e: SSLException) { + logger.warn("Failed to setup gRPC channel", e) + } + } + + private fun shutdownGprsChannel(grpcChannel: ManagedChannel) { + grpcChannel.shutdownNow() + try { + val isShutdown = grpcChannel.awaitTermination(3, TimeUnit.SECONDS) + logger.info("grpcChannel is shutdown : $isShutdown") + } catch (e: InterruptedException) { + logger.info("Error shutting down gRPC channel", e) + } + } + + /** + * Init the gRPC channel that will be used to send/receive + * Diameter messages to the OCS module in Prime. + */ + private fun initCreditControlRequestStream() { + creditControlRequestStream = ocsServiceStub!!.creditControlRequest( + object : StreamObserver { + override fun onNext(answer: CreditControlAnswerInfo?) { + protobufDataSource.handleCcrAnswer(answer!!) + } + + override fun onError(t: Throwable) { + logger.error("CreditControlRequestStream error", t) + if (t is StatusRuntimeException) { + reconnectStreams() + } + } + + override fun onCompleted() { // Nothing to do here + } + }) + } + + /** + * Init the gRPC channel that will be used to get activation requests from the + * OCS. These requests are send when we need to reactivate a diameter session. For + * example on a topup event. + */ + private fun initActivateStream() { + val dummyActivate = ActivateRequest.newBuilder().build() + ocsServiceStub!!.activate(dummyActivate, object : StreamObserver { + override fun onNext(activateResponse: ActivateResponse?) { + protobufDataSource.handleActivateResponse(activateResponse!!) + } + + override fun onError(t: Throwable) { + logger.error("ActivateObserver error", t) + if (t is StatusRuntimeException) { + reconnectStreams() + } + } + + override fun onCompleted() { // Nothing to do here + } + }) + } + + /** + * The keep alive messages are sent on the creditControlRequestStream + * to force it to stay open, avoiding reconnects on the gRPC channel. + */ + private fun initKeepAlive() { + executorService.scheduleWithFixedDelay({ + val ccr = CreditControlRequestInfo.newBuilder() + .setType(CreditControlRequestType.NONE) + .build() + producer.queueEvent(ccr) + }, + 10, + 5, + TimeUnit.SECONDS) + } + + private fun reconnectStreams() { + logger.debug("reconnectStreams called") + if (!isReconnecting) { + reconnectStreamFuture = executorService.schedule(Callable { + logger.debug("Reconnecting gRPC streams") + setupChannel() + initCreditControlRequestStream() + setupEventConsumer() + initActivateStream() + "Called!" + }, + 5, + TimeUnit.SECONDS) + } + } + + private val isReconnecting: Boolean + get() = if (reconnectStreamFuture != null) { + !reconnectStreamFuture!!.isDone + } else false + + override fun handleRequest(context: CreditControlContext) { + val creditControlRequestInfo = protobufDataSource.handleRequest(context, null) + if (creditControlRequestInfo != null) { + context.sentToOcsTime = System.currentTimeMillis() + producer.queueEvent(creditControlRequestInfo) + } else { + logger.error("Failed to handle request ${context.sessionId}") + } + } + + override fun isBlocked(msisdn: String): Boolean { + return protobufDataSource.isBlocked(msisdn) + } + + /** + * Generate a new instance that connects to an endpoint, and + * optionally also encrypts the connection. + * + * @param ocsServerHostname The gRPC endpoint to connect the client to. + * @throws IOException + */ + init { + logger.info("Created GrpcDataSource") + logger.info("ocsServerHostname : {}", ocsServerHostname) + /** + * Not using the standard GOOGLE_APPLICATION_CREDENTIALS for this + * as we need to download the file using container credentials in + * OcsApplication. + **/ + val serviceAccountFile = "/config/" + System.getenv("SERVICE_FILE") + jwtAccessCredentials = ServiceAccountJwtAccessCredentials.fromStream(FileInputStream(serviceAccountFile)) + producer = EventProducer(requestQueue) + } +} \ No newline at end of file diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt similarity index 99% rename from ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt rename to ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt index 0b7c3f0b4..9f9967766 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt @@ -13,7 +13,7 @@ import org.ostelco.diameter.model.MultipleServiceCreditControl import org.ostelco.diameter.model.ResultCode.DIAMETER_UNABLE_TO_COMPLY import org.ostelco.diameter.model.SessionContext import org.ostelco.ocs.api.* -import org.ostelco.ocsgw.OcsServer +import OcsServer import org.ostelco.ocsgw.converter.ProtobufToDiameterConverter import java.util.* import java.util.concurrent.ConcurrentHashMap diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt similarity index 100% rename from ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt rename to ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt diff --git a/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.kt new file mode 100644 index 000000000..42013cc24 --- /dev/null +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/datasource/proxy/ProxyDataSource.kt @@ -0,0 +1,55 @@ +package org.ostelco.ocsgw.datasource.proxy + +import org.ostelco.diameter.CreditControlContext +import org.ostelco.diameter.getLogger +import org.ostelco.ocs.api.CreditControlRequestType +import org.ostelco.ocsgw.datasource.DataSource +import org.ostelco.ocsgw.datasource.local.LocalDataSource + +/** + * Proxy DataSource is a combination of the Local DataSource and any other + * DataSource. + * + * With the proxy approach the CCR-I request will not use the LocalDataStore. If the + * reply for the CCR-I is accepted the following CCR-U requests will use the LocalDataSource + * to quickly reply and accept the query. But it will also send the same request on using the + * secondary DataSource. When the secondary DataSource deny a CCR-U then the next request will be denied. + * + * The DataSource will keep a block list of msisdns that has failed to query new buckets when doing CCR. + */ +class ProxyDataSource(private val secondary: DataSource) : DataSource { + private val local: DataSource = LocalDataSource() + override fun init() { // No init needed + } + + override fun handleRequest(context: CreditControlContext) { + when (context.originalCreditControlRequest.requestTypeAVPValue) { + CreditControlRequestType.INITIAL_REQUEST_VALUE -> secondary.handleRequest(context) + CreditControlRequestType.UPDATE_REQUEST_VALUE -> if (!secondary.isBlocked(context.creditControlRequest.msisdn)) { + proxyAnswer(context) + } else { + secondary.handleRequest(context) + } + CreditControlRequestType.TERMINATION_REQUEST_VALUE -> proxyAnswer(context) + else -> logger.warn("Unknown request type : {}", context.originalCreditControlRequest.requestTypeAVPValue) + } + } + + /** + * Use the local data source to send an answer directly to P-GW. + * Use secondary to report the usage to OCS. + */ + private fun proxyAnswer(context: CreditControlContext) { + local.handleRequest(context) + context.skipAnswer = true + secondary.handleRequest(context) + } + + override fun isBlocked(msisdn: String): Boolean { + return secondary.isBlocked(msisdn) + } + + companion object { + private val logger by getLogger() + } +} \ No newline at end of file diff --git a/ocsgw/src/main/kotlin/org/ostelco/ocsgw/utils/AppConfig.kt b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/utils/AppConfig.kt new file mode 100644 index 000000000..ae70a13fd --- /dev/null +++ b/ocsgw/src/main/kotlin/org/ostelco/ocsgw/utils/AppConfig.kt @@ -0,0 +1,92 @@ +package org.ostelco.ocsgw.utils + +import org.ostelco.diameter.getLogger +import org.ostelco.ocsgw.datasource.DataSourceType +import org.ostelco.ocsgw.datasource.SecondaryDataSourceType +import java.util.* + +class AppConfig { + + private val logger by getLogger() + + private val prop = Properties() + // OCS_DATASOURCE_TYPE env has higher preference over config.properties + val dataStoreType: DataSourceType + get() { // OCS_DATASOURCE_TYPE env has higher preference over config.properties + val dataSource = System.getenv("OCS_DATASOURCE_TYPE") + return if (dataSource.isNullOrEmpty()) { + try { + DataSourceType.valueOf(prop.getProperty("DataStoreType", "Local")) + } catch (e: IllegalArgumentException) { + logger.warn("Could not get the DataSourceType from property") + DataSourceType.Local + } + } else try { + DataSourceType.valueOf(dataSource) + } catch (e: IllegalArgumentException) { + logger.warn("Could not get the DataSourceType from env") + DataSourceType.Local + } + } + + val defaultRequestedServiceUnit: Long + get() { + val defaultRequestedServiceUnit = System.getProperty("DEFAULT_REQUESTED_SERVICE_UNIT") + return if (defaultRequestedServiceUnit.isNullOrEmpty()) { + 40000000L + } else { + defaultRequestedServiceUnit.toLong() + } + } + + // OCS_SECONDARY_DATASOURCE_TYPE env has higher preference over config.properties + val secondaryDataStoreType: SecondaryDataSourceType + get() { // OCS_SECONDARY_DATASOURCE_TYPE env has higher preference over config.properties + val secondaryDataSource = System.getenv("OCS_SECONDARY_DATASOURCE_TYPE") + return if (secondaryDataSource.isNullOrEmpty()) { + try { + SecondaryDataSourceType.valueOf(prop.getProperty("SecondaryDataStoreType", "PubSub")) + } catch (e: IllegalArgumentException) { + SecondaryDataSourceType.PubSub + } + } else try { + SecondaryDataSourceType.valueOf(secondaryDataSource) + } catch (e: IllegalArgumentException) { + logger.warn("Could not get the Secondary DataSourceType") + SecondaryDataSourceType.PubSub + } + } + + val grpcServer: String + get() = getEnvProperty("OCS_GRPC_SERVER") + + val pubSubProjectId: String + get() = getEnvProperty("PUBSUB_PROJECT_ID") + + val pubSubTopicIdForCcr: String + get() = getEnvProperty("PUBSUB_CCR_TOPIC_ID") + + val pubSubTopicIdForCca: String + get() = getEnvProperty("PUBSUB_CCA_TOPIC_ID") + + val pubSubSubscriptionIdForCca: String + get() = getEnvProperty("PUBSUB_CCA_SUBSCRIPTION_ID") + + val pubSubSubscriptionIdForActivate: String + get() = getEnvProperty("PUBSUB_ACTIVATE_SUBSCRIPTION_ID") + + private fun getEnvProperty(propertyName: String): String { + val value = System.getenv(propertyName) + if (value.isNullOrEmpty()) { + throw Error("No $propertyName set in env") + } + return value + } + + init { + val fileName = "config.properties" + val iStream = this.javaClass.classLoader.getResourceAsStream(fileName) + prop.load(iStream) + iStream?.close() + } +} \ No newline at end of file diff --git a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java deleted file mode 100644 index 131c9a42b..000000000 --- a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java +++ /dev/null @@ -1,528 +0,0 @@ -package org.ostelco.ocsgw; - -import org.jdiameter.api.Avp; -import org.jdiameter.api.AvpDataException; -import org.jdiameter.api.AvpSet; -import org.jdiameter.api.Request; -import org.jdiameter.api.Session; -import org.junit.jupiter.api.*; -import org.ostelco.diameter.model.ReAuthRequestType; -import org.ostelco.diameter.model.ReportingReason; -import org.ostelco.diameter.model.RequestType; -import org.ostelco.diameter.model.SessionContext; -import org.ostelco.diameter.test.Result; -import org.ostelco.diameter.test.TestClient; -import org.ostelco.diameter.test.TestHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.xml.bind.DatatypeConverter; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; - - -/** - * Tests for the OcsApplication. This will use a TestClient to - * actually send Diameter traffic on localhost to the OcsApplication. - */ - -@DisplayName("OcsApplicationTest") -public class OcsApplicationTest { - - private static final Logger LOG = LoggerFactory.getLogger(OcsApplicationTest.class); - - private static final int VENDOR_ID_3GPP = 10415; - - private static final String OCS_REALM = "loltel_ocs"; - private static final String OCS_HOST = "ocs"; - private static final String PGW_HOST = "pgw"; - private static final String PGW_REALM = "loltel_pgw"; - private static final String APN = "loltel-test"; - private static final String MCC_MNC = "24201"; - private static final String MSISDN = "4790300123"; - private static final long DIAMETER_SUCCESS = 2001L; - - private static TestClient client; - - // The same OcsApplication will be used in all test cases - private final static OcsApplication application = new OcsApplication(); - - @BeforeAll - public static void setUp() { - application.start("src/test/resources/", "server-jdiameter-config.xml"); - - client = new TestClient(); - client.initStack("src/test/resources/", "client-jdiameter-config.xml"); - } - - @AfterAll - public static void tearDown() { - client.shutdown(); - client = null; - - OcsApplication.shutdown(); - } - - private void simpleCreditControlRequestInit(Session session, Long requestedBucketSize, Long expectedGrantedBucketSize, Integer ratingGroup, Integer serviceIdentifier) { - - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createInitRequest(request.getAvps(), MSISDN, requestedBucketSize, ratingGroup, serviceIdentifier); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(DIAMETER_SUCCESS, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - - if (serviceIdentifier > 0) { - assertEquals(serviceIdentifier.longValue(), resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); - } - - if (ratingGroup > 0) { - assertEquals(ratingGroup.longValue(), resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); - } - - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(expectedGrantedBucketSize.longValue(), granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - } - - private void simpleCreditControlRequestUpdate(Session session, - Long requestedBucketSize, - Long usedBucketSize, - Long expectedGrantedBucketSize, - Integer ratingGroup, - Integer serviceIdentifier) { - - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(DIAMETER_SUCCESS, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - - if (serviceIdentifier > 0) { - assertEquals(serviceIdentifier.longValue(), resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); - } - - if (ratingGroup > 0) { - assertEquals(ratingGroup.longValue(), resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); - } - - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(expectedGrantedBucketSize.longValue(), granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - - } - - @Test - @DisplayName("Simple Credit-Control-Request Init Update and Terminate") - public void simpleCreditControlRequestInitUpdateAndTerminate() { - - final int ratingGroup = 10; - final int serviceIdentifier = 1; - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - simpleCreditControlRequestInit(session, 500_000L, 500_000L,ratingGroup, serviceIdentifier); - simpleCreditControlRequestUpdate(session, 400_000L, 500_000L, 400_000L, ratingGroup, serviceIdentifier); - - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createTerminateRequest(request.getAvps(), MSISDN, 400_000L, ratingGroup, 1); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - session.release(); - } - - - @Test - @DisplayName("Simple Credit-Control-Request Init Update no Requested-Service-Units") - public void simpleCreditControlRequestInitUpdateNoRSU() { - - final int ratingGroup = 10; - final int serviceIdentifier = 1; - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - simpleCreditControlRequestInit(session, 500_000L, 500_000L, ratingGroup, serviceIdentifier); - - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - // Only report usage, no request for new bucket - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, -1L, 500_000L, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - assertEquals(86400L, resultAvps.getAvp(Avp.VALIDITY_TIME).getInteger32()); - - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - session.release(); - } - - - - @Test - @DisplayName("Credit-Control-Request Init Update and Terminate No Requested-Service-Unit Set") - public void CreditControlRequestInitUpdateAndTerminateNoRequestedServiceUnit() { - - final int ratingGroup = 10; - final int serviceIdentifier = -1; - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - simpleCreditControlRequestInit(session, 0L, 40_000_000L, ratingGroup, serviceIdentifier); - simpleCreditControlRequestUpdate(session, 0L, 40_000_000L, 40_000_000L, ratingGroup, serviceIdentifier); - - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createTerminateRequest(request.getAvps(), MSISDN, 40_000_000L, ratingGroup, serviceIdentifier); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - session.release(); - } - - @Test - @DisplayName("Credit-Control-Request Multi Ratinggroups Init") - public void creditControlRequestMultiRatingGroupsInit() { - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createInitRequestMultiRatingGroups(request.getAvps(), MSISDN, 500_000L); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - AvpSet resultMSCCs = resultAvps.getAvps(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(3, resultMSCCs.size()); - for ( int i=0; i < resultMSCCs.size(); i++ ) { - AvpSet mscc = resultMSCCs.getAvpByIndex(i).getGrouped(); - assertEquals(DIAMETER_SUCCESS, mscc.getAvp(Avp.RESULT_CODE).getInteger32()); - Avp granted = mscc.getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(500_000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - int serviceIdentifier = (int) mscc.getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32(); - switch (serviceIdentifier) { - case 1 : - assertEquals(10, mscc.getAvp(Avp.RATING_GROUP).getUnsigned32()); - break; - case 2 : - assertEquals(12, mscc.getAvp(Avp.RATING_GROUP).getUnsigned32()); - break; - case 4 : - assertEquals(14, mscc.getAvp(Avp.RATING_GROUP).getUnsigned32()); - break; - default: - fail("Unexpected Service-Identifier"); - - } - - } - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - } - - @Test - @DisplayName("test AVP not in Diameter dictionary") - public void testUnknownAVP() { - - final int ratingGroup = 10; - final int serviceIdentifier = 1; - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createInitRequest(request.getAvps(), MSISDN, 500_000L, ratingGroup, serviceIdentifier); - TestHelper.addUnknownApv(request.getAvps()); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(DIAMETER_SUCCESS, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - assertEquals(serviceIdentifier, resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); - assertEquals(ratingGroup, resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(500_000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - } - - - @Test - @DisplayName("Test no MSCC in CCR-U") - public void testNoMsccInCcrU() { - - final int ratingGroup = 10; - final int serviceIdentifier = 1; - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - Request initRequest = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createInitRequest(initRequest.getAvps(), MSISDN, 500_000L, ratingGroup, serviceIdentifier); - - client.sendNextRequest(initRequest, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(DIAMETER_SUCCESS, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - assertEquals(serviceIdentifier, resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); - assertEquals(ratingGroup, resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(500_000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - - Request updateRequest = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - TestHelper.createUpdateRequest(updateRequest.getAvps(),MSISDN, -1L, 500_000L, ratingGroup, serviceIdentifier, ReportingReason.QHT); - - client.sendNextRequest(updateRequest, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertNull( "No requested MSCC", resultMSCC); - assertEquals(86400, resultAvps.getAvp(Avp.VALIDITY_TIME).getInteger32()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - } - - @Test - public void testReAuthRequest() { - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - simpleCreditControlRequestInit(session, 500_000L, 500_000L, 10, 1); - - OcsServer.INSTANCE.sendReAuthRequest(new SessionContext(session.getSessionId(), PGW_HOST, PGW_REALM, APN, MCC_MNC)); - waitForRequest(session.getSessionId()); - try { - Result result = client.getRequest(session.getSessionId()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(ReAuthRequestType.AUTHORIZE_ONLY.ordinal(), resultAvps.getAvp(Avp.RE_AUTH_REQUEST_TYPE).getInteger32()); - assertEquals(PGW_HOST, resultAvps.getAvp(Avp.DESTINATION_HOST).getUTF8String()); - assertEquals(PGW_REALM, resultAvps.getAvp(Avp.DESTINATION_REALM).getUTF8String()); - assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - - } catch (AvpDataException e) { - LOG.error("Failed to get Avp", e); - } - session.release(); - } - - // Currently not used in testing - @DisplayName("Service-Information Credit-Control-Request Init") - public void serviceInformationCreditControlRequestInit() throws UnsupportedEncodingException { - - Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - Request request = client.createRequest( - OCS_REALM, - OCS_HOST, - session - ); - - AvpSet ccrAvps = request.getAvps(); - TestHelper.createInitRequest(ccrAvps, MSISDN, 500000L, 10, 1); - - AvpSet serviceInformation = ccrAvps.addGroupedAvp(Avp.SERVICE_INFORMATION, VENDOR_ID_3GPP, true, false); - AvpSet psInformation = serviceInformation.addGroupedAvp(Avp.PS_INFORMATION, VENDOR_ID_3GPP, true, false); - psInformation.addAvp(Avp.TGPP_CHARGING_ID, "01aaacf" , VENDOR_ID_3GPP, true, false, true); - psInformation.addAvp(Avp.TGPP_PDP_TYPE, 0, VENDOR_ID_3GPP, true, false); // IPv4 - try { - psInformation.addAvp(Avp.PDP_ADDRESS, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), VENDOR_ID_3GPP, true, false); - psInformation.addAvp(Avp.SGSN_ADDRESS, InetAddress.getByAddress(new byte[]{8, 0, 0, 6}), VENDOR_ID_3GPP, true, false); - psInformation.addAvp(Avp.GGSN_ADDRESS, InetAddress.getByAddress(new byte[]{2, 0, 0, 6}), VENDOR_ID_3GPP, true, false); - } catch (UnknownHostException e) { - LOG.info("Failed to add address"); - } - psInformation.addAvp(Avp.TGPP_IMSI_MCC_MNC, "24201", VENDOR_ID_3GPP, true, false, false); - psInformation.addAvp(Avp.TGPP_GGSN_MCC_MNC, "24006", VENDOR_ID_3GPP, true, false, false); - psInformation.addAvp(Avp.TGPP_NSAPI, "6", VENDOR_ID_3GPP, true, false, true); - psInformation.addAvp(30, "loltel", false); // Called-Station-Id - psInformation.addAvp(Avp.TGPP_SESSION_STOP_INDICATOR, "\377", VENDOR_ID_3GPP, true, false, false); - psInformation.addAvp(Avp.TGPP_SELECTION_MODE, "0", VENDOR_ID_3GPP, true, false, false); - psInformation.addAvp(Avp.TGPP_CHARGING_CHARACTERISTICS, "0800", VENDOR_ID_3GPP, true, false, true); - psInformation.addAvp(Avp.GPP_SGSN_MCC_MNC, "24201", VENDOR_ID_3GPP, true, false, false); - byte[] timeZoneBytes = new byte[] {64, 00}; - String timeZone = new String(timeZoneBytes, StandardCharsets.UTF_8); - psInformation.addAvp(Avp.TGPP_MS_TIMEZONE, timeZone, VENDOR_ID_3GPP, true, false, true); - psInformation.addAvp(Avp.CHARGING_RULE_BASE_NAME, "RB1", VENDOR_ID_3GPP, true, false, false); - byte[] ratTypeBytes = new byte[] {06}; - String ratType = new String(ratTypeBytes, StandardCharsets.UTF_8); - psInformation.addAvp(Avp.TGPP_RAT_TYPE, ratType , VENDOR_ID_3GPP, true, false, true); - - String s = "8242f21078b542f2100103c703"; - psInformation.addAvp(Avp.GPP_USER_LOCATION_INFO, DatatypeConverter.parseHexBinary(s), VENDOR_ID_3GPP, true, false); - - client.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = client.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(4012L, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - assertEquals(1, resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getInteger32()); - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(0L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - LOG.error("Failed to get Result-Code", e); - } - } - - private void waitForAnswer(String sessionId) { - int i = 0; - while (!client.isAnswerReceived(sessionId) && i<100) { - i++; - try { - Thread.sleep(50); - } catch (InterruptedException e) { - // continue - } - } - assertEquals(true, client.isAnswerReceived(sessionId)); - } - - private void waitForRequest(String sessionId) { - int i = 0; - while (!client.isRequestReceived(sessionId) && i<100) { - i++; - try { - Thread.sleep(50); - } catch (InterruptedException e) { - // continue - } - } - assertEquals(true, client.isRequestReceived(sessionId)); - } -} \ No newline at end of file diff --git a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java deleted file mode 100644 index bdc97ce47..000000000 --- a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java +++ /dev/null @@ -1,319 +0,0 @@ -package org.ostelco.ocsgw; - -import org.jdiameter.api.*; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.ostelco.diameter.model.ReportingReason; -import org.ostelco.diameter.model.RequestType; -import org.ostelco.diameter.test.Result; -import org.ostelco.diameter.test.TestClient; -import org.ostelco.diameter.test.TestHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.channels.SocketChannel; - -import static org.junit.Assert.assertEquals; - - -/** - * Tests for the OcsApplication. This will use a TestClient to - * actually send Diameter traffic on localhost to the OcsApplication. - */ - -@DisplayName("OcsHATest") -public class OcsHATest { - - private static final Logger logger = LoggerFactory.getLogger(OcsHATest.class); - - - private static final String OCS_REALM = "loltel"; - private static final String OCS_HOST_1 = "ocs_1"; - private static final String OCS_HOST_2 = "ocs_2"; - - private static final long DIAMETER_SUCCESS = 2001L; - - private static final String MSISDN = "4790300123"; - - private TestClient testPGW; - - private Process ocsgw_1; - private Process ocsgw_2; - - - private void waitForServerToStart(final int server) { - switch (server) { - case 1: - waitForPort("127.0.0.1", 3868,10000); - break; - case 2: - waitForPort("127.0.0.1", 3869,10000); - break; - default: - } - } - - private void waitForPort(String hostname, int port, long timeoutMs) { - logger.debug("Waiting for port " + port); - long startTs = System.currentTimeMillis(); - boolean scanning = true; - while (scanning) - { - if (System.currentTimeMillis() - startTs > timeoutMs) { - logger.error("Timeout waiting for port " + port); - scanning = false; - } - try - { - SocketAddress addr = new InetSocketAddress(hostname, port); - SocketChannel socketChannel = SocketChannel.open(); - socketChannel.configureBlocking(true); - try { - socketChannel.connect(addr); - } - finally { - socketChannel.close(); - } - - scanning = false; - } - catch(IOException e) - { - logger.debug("Still waiting for port " + port); - try - { - Thread.sleep(2000);//2 seconds - } - catch(InterruptedException ie){ - logger.error("Interrupted", ie); - } - } - } - logger.debug("Port " + port + " ready."); - } - - private void waitForProcessExit(Process process) { - while (process.isAlive()) { - try - { - Thread.sleep(2000);//2 seconds - } - catch(InterruptedException ie){ - logger.error("Interrupted", ie); - } - } - } - - private Process startServer(final int server) { - - Process process = null; - ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", "./build/libs/ocsgw-uber.jar"); - processBuilder.environment().put("DIAMETER_CONFIG_FILE", "server-jdiameter-ha-" + server +"-config.xml"); - processBuilder.environment().put("OCS_DATASOURCE_TYPE", "Local"); - processBuilder.environment().put("CONFIG_FOLDER", "src/test/resources/"); - processBuilder.inheritIO(); - try { - process = processBuilder.start(); - } catch (IOException e) { - logger.error("Failed to start external OCSgw number" + server, e); - } - return process; - } - - - @BeforeEach - protected void setUp() { - logger.debug("setUp()"); - - ocsgw_1 = startServer(1); - ocsgw_2 = startServer(2); - - waitForServerToStart(1); - waitForServerToStart(2); - - testPGW = new TestClient(); - testPGW.initStack("src/test/resources/", "client-jdiameter-ha-config.xml"); - } - - @AfterEach - protected void tearDown() { - logger.debug("tearDown()"); - testPGW.shutdown(); - testPGW = null; - - ocsgw_1.destroy(); - ocsgw_2.destroy(); - } - - private void haCreditControlRequestInit(Session session, String host) { - - Request request = testPGW.createRequest( - OCS_REALM, - host, - session - ); - - TestHelper.createInitRequest(request.getAvps(), MSISDN, 500000L, 1, 10); - - testPGW.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = testPGW.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(host, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(2001L, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - assertEquals(10, resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); - assertEquals(1, resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(500000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - logger.error("Failed to get Result-Code", e); - } - } - - private void restartServer(final int server) { - switch (server) { - case 1: - stopServer(1); - waitForProcessExit(ocsgw_1); - ocsgw_1 = startServer(1); - waitForServerToStart(1); - break; - case 2: - stopServer(2); - waitForProcessExit(ocsgw_2); - ocsgw_2 = startServer(2); - waitForServerToStart(2); - break; - default: - logger.info("Incorrect server number : " + server); - } - // Give time for ocsgw to reconnect to P-GW - try - { - logger.debug("Pausing testing 10 seconds so that ocsgw can reconnect..."); - Thread.sleep(10000);//10 seconds - logger.debug("Continue testing"); - } - catch(InterruptedException ie){ - logger.error("Interrupted", ie); - } - } - - private void stopServer(final int server) { - switch (server) { - case 1: - ocsgw_1.destroy(); - break; - case 2: - ocsgw_2.destroy(); - break; - default: - } - } - - private void haCreditControlRequestUpdate(Session session, String host) { - - Request request = testPGW.createRequest( - OCS_REALM, - host, - session - ); - - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, 400000L, 500000L, 1, 10, ReportingReason.QUOTA_EXHAUSTED); - - testPGW.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = testPGW.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(host, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); - assertEquals(2001L, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); - Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); - assertEquals(400000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); - } catch (AvpDataException e) { - logger.error("Failed to get Result-Code", e); - } - } - - /** - * This is only meant to be used for local testing. As it is starting external processes for the - * OCSgw you will have to take care to clean up. - * - * The test will create a session. It will restart server 1 before it sends the CCR-Update message. - * Then restart server 2 and send another CCR-Update message. This will only work if state was shared - * as the session would otherwise have been lost by ocsgw. - */ - @DisplayName("HA Credit-Control-Request Init Update and Terminate") - //@Test - public void haCreditControlRequestInitUpdateAndTerminate() { - Session session = testPGW.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); - haCreditControlRequestInit(session, OCS_HOST_1); - - // Restart server 1 and continue when it is back online - restartServer(1); - - haCreditControlRequestUpdate(session, OCS_HOST_1); - - // Stop server 1 and hand over request to server 2 - stopServer(1); - haCreditControlRequestUpdate(session, OCS_HOST_2); - - // Restart server 2 and continue once it is back up - restartServer(2); - - Request request = testPGW.createRequest( - OCS_REALM, - OCS_HOST_2, - session - ); - - TestHelper.createTerminateRequest(request.getAvps(), MSISDN, 700000L, 1, 10); - - testPGW.sendNextRequest(request, session); - - waitForAnswer(session.getSessionId()); - - try { - Result result = testPGW.getAnswer(session.getSessionId()); - assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); - AvpSet resultAvps = result.getResultAvps(); - assertEquals(OCS_HOST_2, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); - assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); - assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); - } catch (AvpDataException e) { - logger.error("Failed to get Result-Code", e); - } - session.release(); - } - - private void waitForAnswer(String sessionId) { - int i = 0; - while (!testPGW.isAnswerReceived(sessionId) && i<10) { - i++; - try { - Thread.sleep(500); - } catch (InterruptedException e) { - // continue - } - } - assertEquals(true, testPGW.isAnswerReceived(sessionId)); - } -} \ No newline at end of file diff --git a/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsApplicationTest.kt b/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsApplicationTest.kt new file mode 100644 index 000000000..4ca2bab4b --- /dev/null +++ b/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsApplicationTest.kt @@ -0,0 +1,439 @@ +package org.ostelco.ocsgw + +import OcsServer +import org.jdiameter.api.Avp +import org.jdiameter.api.AvpDataException +import org.jdiameter.api.Session +import org.junit.Assert +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.ostelco.diameter.model.ReAuthRequestType +import org.ostelco.diameter.model.ReportingReason +import org.ostelco.diameter.model.RequestType +import org.ostelco.diameter.model.SessionContext +import org.ostelco.diameter.test.TestClient +import org.ostelco.diameter.test.TestHelper.addUnknownApv +import org.ostelco.diameter.test.TestHelper.createInitRequest +import org.ostelco.diameter.test.TestHelper.createInitRequestMultiRatingGroups +import org.ostelco.diameter.test.TestHelper.createTerminateRequest +import org.ostelco.diameter.test.TestHelper.createUpdateRequest +import org.slf4j.LoggerFactory +import java.io.UnsupportedEncodingException +import java.net.InetAddress +import java.net.UnknownHostException +import java.nio.charset.StandardCharsets +import javax.xml.bind.DatatypeConverter + +/** + * Tests for the OcsApplication. This will use a TestClient to + * actually send Diameter traffic on localhost to the OcsApplication. + */ +@DisplayName("OcsApplicationTest") +class OcsApplicationTest { + private fun simpleCreditControlRequestInit(session: Session?, requestedBucketSize: Long, expectedGrantedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int) { + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createInitRequest(request!!.avps, MSISDN, requestedBucketSize, ratingGroup, serviceIdentifier) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + if (serviceIdentifier > 0) { + Assert.assertEquals(serviceIdentifier.toLong(), resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32) + } + if (ratingGroup > 0) { + Assert.assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).unsigned32) + } + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(expectedGrantedBucketSize, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + private fun simpleCreditControlRequestUpdate(session: Session?, + requestedBucketSize: Long, + usedBucketSize: Long, + expectedGrantedBucketSize: Long, + ratingGroup: Int, + serviceIdentifier: Int) { + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createUpdateRequest(request!!.avps, MSISDN, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + if (serviceIdentifier > 0) { + Assert.assertEquals(serviceIdentifier.toLong(), resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32) + } + if (ratingGroup > 0) { + Assert.assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).unsigned32) + } + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(expectedGrantedBucketSize, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + @Test + @DisplayName("Simple Credit-Control-Request Init Update and Terminate") + fun simpleCreditControlRequestInitUpdateAndTerminate() { + val ratingGroup = 10 + val serviceIdentifier = 1 + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + simpleCreditControlRequestInit(session, 500000L, 500000L, ratingGroup, serviceIdentifier) + simpleCreditControlRequestUpdate(session, 400000L, 500000L, 400000L, ratingGroup, serviceIdentifier) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createTerminateRequest(request!!.avps, MSISDN, 400000L, ratingGroup, 1) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + session.release() + } + + @Test + @DisplayName("Simple Credit-Control-Request Init Update no Requested-Service-Units") + fun simpleCreditControlRequestInitUpdateNoRSU() { + val ratingGroup = 10 + val serviceIdentifier = 1 + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + simpleCreditControlRequestInit(session, 500000L, 500000L, ratingGroup, serviceIdentifier) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + // Only report usage, no request for new bucket + createUpdateRequest(request!!.avps, MSISDN, -1L, 500000L, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + Assert.assertEquals(86400L, resultAvps.getAvp(Avp.VALIDITY_TIME).integer32.toLong()) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + session.release() + } + + @Test + @DisplayName("Credit-Control-Request Init Update and Terminate No Requested-Service-Unit Set") + fun CreditControlRequestInitUpdateAndTerminateNoRequestedServiceUnit() { + val ratingGroup = 10 + val serviceIdentifier = -1 + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + simpleCreditControlRequestInit(session, 0L, 40000000L, ratingGroup, serviceIdentifier) + simpleCreditControlRequestUpdate(session, 0L, 40000000L, 40000000L, ratingGroup, serviceIdentifier) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createTerminateRequest(request!!.avps, MSISDN, 40000000L, ratingGroup, serviceIdentifier) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + session.release() + } + + @Test + @DisplayName("Credit-Control-Request Multi Ratinggroups Init") + fun creditControlRequestMultiRatingGroupsInit() { + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createInitRequestMultiRatingGroups(request!!.avps, MSISDN, 500000L) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCCs = resultAvps.getAvps(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(3, resultMSCCs.size().toLong()) + for (i in 0 until resultMSCCs.size()) { + val mscc = resultMSCCs.getAvpByIndex(i).grouped + Assert.assertEquals(DIAMETER_SUCCESS, mscc.getAvp(Avp.RESULT_CODE).integer32.toLong()) + val granted = mscc.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(500000L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + val serviceIdentifier = mscc.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32.toInt() + when (serviceIdentifier) { + 1 -> Assert.assertEquals(10, mscc.getAvp(Avp.RATING_GROUP).unsigned32) + 2 -> Assert.assertEquals(12, mscc.getAvp(Avp.RATING_GROUP).unsigned32) + 4 -> Assert.assertEquals(14, mscc.getAvp(Avp.RATING_GROUP).unsigned32) + else -> Assert.fail("Unexpected Service-Identifier") + } + } + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + @Test + @DisplayName("test AVP not in Diameter dictionary") + fun testUnknownAVP() { + val ratingGroup = 10 + val serviceIdentifier = 1 + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createInitRequest(request!!.avps, MSISDN, 500000L, ratingGroup, serviceIdentifier) + addUnknownApv(request.avps) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + Assert.assertEquals(serviceIdentifier.toLong(), resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32) + Assert.assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).unsigned32) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(500000L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + @Test + @DisplayName("Test no MSCC in CCR-U") + fun testNoMsccInCcrU() { + val ratingGroup = 10 + val serviceIdentifier = 1 + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + val initRequest = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + createInitRequest(initRequest!!.avps, MSISDN, 500000L, ratingGroup, serviceIdentifier) + client!!.sendNextRequest(initRequest, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + Assert.assertEquals(serviceIdentifier.toLong(), resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32) + Assert.assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).unsigned32) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(500000L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + val updateRequest = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session + ) + createUpdateRequest(updateRequest!!.avps, MSISDN, -1L, 500000L, ratingGroup, serviceIdentifier, ReportingReason.QHT) + client!!.sendNextRequest(updateRequest, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertNull("No requested MSCC", resultMSCC) + Assert.assertEquals(86400, resultAvps.getAvp(Avp.VALIDITY_TIME).integer32.toLong()) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + @Test + fun testReAuthRequest() { + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + simpleCreditControlRequestInit(session, 500000L, 500000L, 10, 1) + OcsServer.sendReAuthRequest(SessionContext(session!!.sessionId, PGW_HOST, PGW_REALM, APN, MCC_MNC)) + waitForRequest(session.sessionId) + try { + val result = client!!.getRequest(session.sessionId) + val resultAvps = result!!.resultAvps + Assert.assertEquals(ReAuthRequestType.AUTHORIZE_ONLY.ordinal.toLong(), resultAvps.getAvp(Avp.RE_AUTH_REQUEST_TYPE).integer32.toLong()) + Assert.assertEquals(PGW_HOST, resultAvps.getAvp(Avp.DESTINATION_HOST).utF8String) + Assert.assertEquals(PGW_REALM, resultAvps.getAvp(Avp.DESTINATION_REALM).utF8String) + Assert.assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + } catch (e: AvpDataException) { + LOG.error("Failed to get Avp", e) + } + session.release() + } + + // Currently not used in testing + @DisplayName("Service-Information Credit-Control-Request Init") + @Throws(UnsupportedEncodingException::class) + fun serviceInformationCreditControlRequestInit() { + val session = client!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + val request = client!!.createRequest( + OCS_REALM, + OCS_HOST, + session!! + ) + val ccrAvps = request!!.avps + createInitRequest(ccrAvps, MSISDN, 500000L, 10, 1) + val serviceInformation = ccrAvps.addGroupedAvp(Avp.SERVICE_INFORMATION, VENDOR_ID_3GPP.toLong(), true, false) + val psInformation = serviceInformation.addGroupedAvp(Avp.PS_INFORMATION, VENDOR_ID_3GPP.toLong(), true, false) + psInformation.addAvp(Avp.TGPP_CHARGING_ID, "01aaacf", VENDOR_ID_3GPP.toLong(), true, false, true) + psInformation.addAvp(Avp.TGPP_PDP_TYPE, 0, VENDOR_ID_3GPP.toLong(), true, false) // IPv4 + try { + psInformation.addAvp(Avp.PDP_ADDRESS, InetAddress.getByAddress(byteArrayOf(127, 0, 0, 1)), VENDOR_ID_3GPP.toLong(), true, false) + psInformation.addAvp(Avp.SGSN_ADDRESS, InetAddress.getByAddress(byteArrayOf(8, 0, 0, 6)), VENDOR_ID_3GPP.toLong(), true, false) + psInformation.addAvp(Avp.GGSN_ADDRESS, InetAddress.getByAddress(byteArrayOf(2, 0, 0, 6)), VENDOR_ID_3GPP.toLong(), true, false) + } catch (e: UnknownHostException) { + LOG.info("Failed to add address") + } + psInformation.addAvp(Avp.TGPP_IMSI_MCC_MNC, "24201", VENDOR_ID_3GPP.toLong(), true, false, false) + psInformation.addAvp(Avp.TGPP_GGSN_MCC_MNC, "24006", VENDOR_ID_3GPP.toLong(), true, false, false) + psInformation.addAvp(Avp.TGPP_NSAPI, "6", VENDOR_ID_3GPP.toLong(), true, false, true) + psInformation.addAvp(30, "loltel", false) // Called-Station-Id + psInformation.addAvp(Avp.TGPP_SESSION_STOP_INDICATOR, "\u00ff", VENDOR_ID_3GPP.toLong(), true, false, false) + psInformation.addAvp(Avp.TGPP_SELECTION_MODE, "0", VENDOR_ID_3GPP.toLong(), true, false, false) + psInformation.addAvp(Avp.TGPP_CHARGING_CHARACTERISTICS, "0800", VENDOR_ID_3GPP.toLong(), true, false, true) + psInformation.addAvp(Avp.GPP_SGSN_MCC_MNC, "24201", VENDOR_ID_3GPP.toLong(), true, false, false) + val timeZoneBytes = byteArrayOf(64, 0) + val timeZone = String(timeZoneBytes, StandardCharsets.UTF_8) + psInformation.addAvp(Avp.TGPP_MS_TIMEZONE, timeZone, VENDOR_ID_3GPP.toLong(), true, false, true) + psInformation.addAvp(Avp.CHARGING_RULE_BASE_NAME, "RB1", VENDOR_ID_3GPP.toLong(), true, false, false) + val ratTypeBytes = byteArrayOf(6) + val ratType = String(ratTypeBytes, StandardCharsets.UTF_8) + psInformation.addAvp(Avp.TGPP_RAT_TYPE, ratType, VENDOR_ID_3GPP.toLong(), true, false, true) + val s = "8242f21078b542f2100103c703" + psInformation.addAvp(Avp.GPP_USER_LOCATION_INFO, DatatypeConverter.parseHexBinary(s), VENDOR_ID_3GPP.toLong(), true, false) + client!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = client!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(4012L, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + Assert.assertEquals(1, resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(0L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + LOG.error("Failed to get Result-Code", e) + } + } + + private fun waitForAnswer(sessionId: String) { + var i = 0 + while (!client!!.isAnswerReceived(sessionId) && i < 100) { + i++ + try { + Thread.sleep(50) + } catch (e: InterruptedException) { // continue + } + } + Assert.assertEquals(true, client!!.isAnswerReceived(sessionId)) + } + + private fun waitForRequest(sessionId: String) { + var i = 0 + while (!client!!.isRequestReceived(sessionId) && i < 100) { + i++ + try { + Thread.sleep(50) + } catch (e: InterruptedException) { // continue + } + } + Assert.assertEquals(true, client!!.isRequestReceived(sessionId)) + } + + companion object { + private val LOG = LoggerFactory.getLogger(OcsApplicationTest::class.java) + private const val VENDOR_ID_3GPP = 10415 + private const val OCS_REALM = "loltel_ocs" + private const val OCS_HOST = "ocs" + private const val PGW_HOST = "pgw" + private const val PGW_REALM = "loltel_pgw" + private const val APN = "loltel-test" + private const val MCC_MNC = "24201" + private const val MSISDN = "4790300123" + private const val DIAMETER_SUCCESS = 2001L + private var client: TestClient? = null + // The same OcsApplication will be used in all test cases + private val application = OcsApplication() + + @BeforeAll + @JvmStatic + fun setUp() { + application.start("src/test/resources/", "server-jdiameter-config.xml") + client = TestClient() + client!!.initStack("src/test/resources/", "client-jdiameter-config.xml") + } + + @AfterAll + @JvmStatic + fun tearDown() { + client!!.shutdown() + client = null + OcsApplication.shutdown() + } + } +} \ No newline at end of file diff --git a/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsHATest.kt b/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsHATest.kt new file mode 100644 index 000000000..e19aa219f --- /dev/null +++ b/ocsgw/src/test/kotlin/org/ostelco/ocsgw/OcsHATest.kt @@ -0,0 +1,264 @@ +package org.ostelco.ocsgw + +import org.jdiameter.api.Avp +import org.jdiameter.api.AvpDataException +import org.jdiameter.api.Session +import org.junit.Assert +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.ostelco.diameter.model.ReportingReason +import org.ostelco.diameter.model.RequestType +import org.ostelco.diameter.test.TestClient +import org.ostelco.diameter.test.TestHelper.createInitRequest +import org.ostelco.diameter.test.TestHelper.createTerminateRequest +import org.ostelco.diameter.test.TestHelper.createUpdateRequest +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.InetSocketAddress +import java.net.SocketAddress +import java.nio.channels.SocketChannel + +/** + * Tests for the OcsApplication. This will use a TestClient to + * actually send Diameter traffic on localhost to the OcsApplication. + */ +@DisplayName("OcsHATest") +class OcsHATest { + private var testPGW: TestClient? = null + private var ocsgw_1: Process? = null + private var ocsgw_2: Process? = null + private fun waitForServerToStart(server: Int) { + when (server) { + 1 -> waitForPort("127.0.0.1", 3868, 10000) + 2 -> waitForPort("127.0.0.1", 3869, 10000) + else -> { + } + } + } + + private fun waitForPort(hostname: String, port: Int, timeoutMs: Long) { + logger.debug("Waiting for port $port") + val startTs = System.currentTimeMillis() + var scanning = true + while (scanning) { + if (System.currentTimeMillis() - startTs > timeoutMs) { + logger.error("Timeout waiting for port $port") + scanning = false + } + try { + val addr: SocketAddress = InetSocketAddress(hostname, port) + val socketChannel = SocketChannel.open() + socketChannel.configureBlocking(true) + try { + socketChannel.connect(addr) + } finally { + socketChannel.close() + } + scanning = false + } catch (e: IOException) { + logger.debug("Still waiting for port $port") + try { + Thread.sleep(2000) //2 seconds + } catch (ie: InterruptedException) { + logger.error("Interrupted", ie) + } + } + } + logger.debug("Port $port ready.") + } + + private fun waitForProcessExit(process: Process?) { + while (process!!.isAlive) { + try { + Thread.sleep(2000) //2 seconds + } catch (ie: InterruptedException) { + logger.error("Interrupted", ie) + } + } + } + + private fun startServer(server: Int): Process? { + var process: Process? = null + val processBuilder = ProcessBuilder("java", "-jar", "./build/libs/ocsgw-uber.jar") + processBuilder.environment()["DIAMETER_CONFIG_FILE"] = "server-jdiameter-ha-$server-config.xml" + processBuilder.environment()["OCS_DATASOURCE_TYPE"] = "Local" + processBuilder.environment()["CONFIG_FOLDER"] = "src/test/resources/" + processBuilder.inheritIO() + try { + process = processBuilder.start() + } catch (e: IOException) { + logger.error("Failed to start external OCSgw number$server", e) + } + return process + } + + @BeforeEach + protected fun setUp() { + logger.debug("setUp()") + ocsgw_1 = startServer(1) + ocsgw_2 = startServer(2) + waitForServerToStart(1) + waitForServerToStart(2) + testPGW = TestClient() + testPGW!!.initStack("src/test/resources/", "client-jdiameter-ha-config.xml") + } + + @AfterEach + protected fun tearDown() { + logger.debug("tearDown()") + testPGW!!.shutdown() + testPGW = null + ocsgw_1!!.destroy() + ocsgw_2!!.destroy() + } + + private fun haCreditControlRequestInit(session: Session?, host: String) { + val request = testPGW!!.createRequest( + OCS_REALM, + host, + session!! + ) + createInitRequest(request!!.avps, MSISDN, 500000L, 1, 10) + testPGW!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = testPGW!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(host, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(2001L, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + Assert.assertEquals(10, resultMSCC.grouped.getAvp(Avp.SERVICE_IDENTIFIER_CCA).unsigned32) + Assert.assertEquals(1, resultMSCC.grouped.getAvp(Avp.RATING_GROUP).unsigned32) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(500000L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + logger.error("Failed to get Result-Code", e) + } + } + + private fun restartServer(server: Int) { + when (server) { + 1 -> { + stopServer(1) + waitForProcessExit(ocsgw_1) + ocsgw_1 = startServer(1) + waitForServerToStart(1) + } + 2 -> { + stopServer(2) + waitForProcessExit(ocsgw_2) + ocsgw_2 = startServer(2) + waitForServerToStart(2) + } + else -> logger.info("Incorrect server number : $server") + } + // Give time for ocsgw to reconnect to P-GW + try { + logger.debug("Pausing testing 10 seconds so that ocsgw can reconnect...") + Thread.sleep(10000) //10 seconds + logger.debug("Continue testing") + } catch (ie: InterruptedException) { + logger.error("Interrupted", ie) + } + } + + private fun stopServer(server: Int) { + when (server) { + 1 -> ocsgw_1!!.destroy() + 2 -> ocsgw_2!!.destroy() + else -> { + } + } + } + + private fun haCreditControlRequestUpdate(session: Session?, host: String) { + val request = testPGW!!.createRequest( + OCS_REALM, + host, + session!! + ) + createUpdateRequest(request!!.avps, MSISDN, 400000L, 500000L, 1, 10, ReportingReason.QUOTA_EXHAUSTED) + testPGW!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = testPGW!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(host, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + Assert.assertEquals(2001L, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + Assert.assertEquals(400000L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } catch (e: AvpDataException) { + logger.error("Failed to get Result-Code", e) + } + } + + /** + * This is only meant to be used for local testing. As it is starting external processes for the + * OCSgw you will have to take care to clean up. + * + * The test will create a session. It will restart server 1 before it sends the CCR-Update message. + * Then restart server 2 and send another CCR-Update message. This will only work if state was shared + * as the session would otherwise have been lost by ocsgw. + */ + @DisplayName("HA Credit-Control-Request Init Update and Terminate") //@Test + fun haCreditControlRequestInitUpdateAndTerminate() { + val session = testPGW!!.createSession(object : Any() {}.javaClass.enclosingMethod.name) + haCreditControlRequestInit(session, OCS_HOST_1) + // Restart server 1 and continue when it is back online + restartServer(1) + haCreditControlRequestUpdate(session, OCS_HOST_1) + // Stop server 1 and hand over request to server 2 + stopServer(1) + haCreditControlRequestUpdate(session, OCS_HOST_2) + // Restart server 2 and continue once it is back up + restartServer(2) + val request = testPGW!!.createRequest( + OCS_REALM, + OCS_HOST_2, + session!! + ) + createTerminateRequest(request!!.avps, MSISDN, 700000L, 1, 10) + testPGW!!.sendNextRequest(request, session) + waitForAnswer(session.sessionId) + try { + val result = testPGW!!.getAnswer(session.sessionId) + Assert.assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + Assert.assertEquals(OCS_HOST_2, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + Assert.assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + Assert.assertEquals(RequestType.TERMINATION_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + } catch (e: AvpDataException) { + logger.error("Failed to get Result-Code", e) + } + session.release() + } + + private fun waitForAnswer(sessionId: String) { + var i = 0 + while (!testPGW!!.isAnswerReceived(sessionId) && i < 10) { + i++ + try { + Thread.sleep(500) + } catch (e: InterruptedException) { // continue + } + } + Assert.assertEquals(true, testPGW!!.isAnswerReceived(sessionId)) + } + + companion object { + private val logger = LoggerFactory.getLogger(OcsHATest::class.java) + private const val OCS_REALM = "loltel" + private const val OCS_HOST_1 = "ocs_1" + private const val OCS_HOST_2 = "ocs_2" + private const val DIAMETER_SUCCESS = 2001L + private const val MSISDN = "4790300123" + } +} \ No newline at end of file