package org.mobicents.servers.diameter.charging;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.jdiameter.api.Answer;
import org.jdiameter.api.ApplicationId;
import org.jdiameter.api.Avp;
import org.jdiameter.api.AvpDataException;
import org.jdiameter.api.AvpSet;
import org.jdiameter.api.EventListener;
import org.jdiameter.api.InternalException;
import org.jdiameter.api.Mode;
import org.jdiameter.api.Network;
import org.jdiameter.api.NetworkReqListener;
import org.jdiameter.api.Peer;
import org.jdiameter.api.Request;
import org.jdiameter.api.ResultCode;
import org.jdiameter.api.app.AppAnswerEvent;
import org.jdiameter.api.app.AppRequestEvent;
import org.jdiameter.api.app.AppSession;
import org.jdiameter.api.auth.events.ReAuthAnswer;
import org.jdiameter.api.auth.events.ReAuthRequest;
import org.jdiameter.api.cca.ClientCCASession;
import org.jdiameter.api.cca.ServerCCASession;
import org.jdiameter.api.cca.events.JCreditControlAnswer;
import org.jdiameter.api.cca.events.JCreditControlRequest;
import org.jdiameter.client.api.ISessionFactory;
import org.jdiameter.common.impl.app.cca.CCASessionFactoryImpl;
import org.jdiameter.common.impl.app.cca.JCreditControlAnswerImpl;
import org.jdiameter.server.impl.app.cca.ServerCCASessionImpl;
import org.mobicents.diameter.dictionary.AvpDictionary;
import org.mobicents.servers.diameter.utils.DiameterUtilities;
import org.mobicents.servers.diameter.utils.StackCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Mobicents Diameter Charging Server Simulator.
*
* @author <a href="mailto:brainslog@gmail.com"> Alexandre Mendonca </a>
*/
public class ChargingServerSimulator extends CCASessionFactoryImpl implements NetworkReqListener, EventListener<Request, Answer> {
private static final Logger logger = LoggerFactory.getLogger(ChargingServerSimulator.class);
private static final Object[] EMPTY_ARRAY = new Object[]{};
private ApplicationId roAppId = ApplicationId.createByAuthAppId(10415L, 4L);
private HashMap<String, Long> accounts = new HashMap<String, Long>();
private HashMap<String, Long> reserved = new HashMap<String, Long>();
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
new ChargingServerSimulator();
}
StackCreator stackCreator = null;
public ChargingServerSimulator() throws Exception {
super();
AvpDictionary.INSTANCE.parseDictionary(this.getClass().getClassLoader().getResourceAsStream("dictionary.xml"));
try {
String config = readFile(this.getClass().getClassLoader().getResourceAsStream("config-server.xml"));
this.stackCreator = new StackCreator(config, this, this, "Server", true);
Network network = this.stackCreator.unwrap(Network.class);
network.addNetworkReqListener(this, roAppId);
network.addNetworkReqListener(this, ApplicationId.createByAuthAppId(0, 4));
this.stackCreator.start(Mode.ALL_PEERS, 30000, TimeUnit.MILLISECONDS);
printLogo();
sessionFactory = (ISessionFactory) stackCreator.getSessionFactory();
init(sessionFactory); // damn.. this doesn't looks good
sessionFactory.registerAppFacory(ServerCCASession.class, this);
sessionFactory.registerAppFacory(ClientCCASession.class, this);
// Read users from properties file
Properties properties = new Properties();
try {
InputStream is = this.getClass().getClassLoader().getResourceAsStream("accounts.properties");
if (is == null) {
throw new IOException("InputStream is null");
}
properties.load(is);
for (Object property : properties.keySet()) {
String accountName = (String) property;
String balance = properties.getProperty(accountName, "0");
if (logger.isInfoEnabled()) {
logger.info("Provisioned user '" + accountName + "' with [" + balance + "] units.");
}
accounts.put(accountName, Long.valueOf(balance));
}
}
catch (IOException e) {
System.err.println("Failed to read 'accounts.properties' file. Aborting.");
System.exit(-1);
}
}
catch (Exception e) {
logger.error("Failure initializing Mobicents Diameter Ro/Rf Server Simulator", e);
}
}
private void printLogo() {
if (logger.isInfoEnabled()) {
Properties sysProps = System.getProperties();
String osLine = sysProps.getProperty("os.name") + "/" + sysProps.getProperty("os.arch");
String javaLine = sysProps.getProperty("java.vm.vendor") + " " + sysProps.getProperty("java.vm.name") + " " + sysProps.getProperty("java.vm.version");
Peer localPeer = stackCreator.getMetaData().getLocalPeer();
String diameterLine = localPeer.getProductName() + " (" + localPeer.getUri() + " @ " + localPeer.getRealmName() + ")";
logger.info("===============================================================================");
logger.info("");
logger.info("== Mobicents Diameter Ro/Rf Server Simulator (" + osLine + ")" );
logger.info("");
logger.info("== " + javaLine);
logger.info("");
logger.info("== " + diameterLine);
logger.info("");
logger.info("===============================================================================");
}
}
@Override
public Answer processRequest(Request request) {
if (logger.isInfoEnabled()) {
logger.info("<< Received Request [" + request + "]");
}
try {
ServerCCASessionImpl session =
(sessionFactory).getNewAppSession(request.getSessionId(), ApplicationId.createByAuthAppId(0, 4), ServerCCASession.class, EMPTY_ARRAY);
session.processRequest(request);
}
catch (InternalException e) {
logger.error(">< Failure handling received request.", e);
}
return null;
}
@Override
public void receivedSuccessMessage(Request request, Answer answer) {
if (logger.isInfoEnabled()) {
logger.info("<< Received Success Message for Request [" + request + "] and Answer [" + answer + "]");
}
}
@Override
public void timeoutExpired(Request request) {
if (logger.isInfoEnabled()) {
logger.info("<< Received Timeout for Request [" + request + "]");
}
}
@Override
public void doCreditControlAnswer(ClientCCASession session, JCreditControlRequest request, JCreditControlAnswer answer) throws InternalException {
// Do nothing.
}
@Override
public void doOtherEvent(AppSession session, AppRequestEvent request, AppAnswerEvent answer) throws InternalException {
// Do nothing.
}
@Override
public void doReAuthRequest(ClientCCASession session, ReAuthRequest request) throws InternalException {
// Do nothing.
}
@Override
public void doCreditControlRequest(ServerCCASession session, JCreditControlRequest request) throws InternalException {
AvpSet ccrAvps = request.getMessage().getAvps();
switch (request.getRequestTypeAVPValue()) {
// INITIAL_REQUEST 1
case 1:
// UPDATE_REQUEST 2
case 2:
if (logger.isInfoEnabled()) {
logger.info("<< Received Credit-Control-Request [" + (request.getRequestTypeAVPValue() == 1 ? "INITIAL" : "UPDATE") + "]");
}
JCreditControlAnswer cca = null;
try {
long requestedUnits = ccrAvps.getAvp(Avp.REQUESTED_SERVICE_UNIT).getGrouped().getAvp(Avp.CC_TIME).getInteger32();
String subscriptionId = ccrAvps.getAvp(Avp.SUBSCRIPTION_ID).getGrouped().getAvp(Avp.SUBSCRIPTION_ID_DATA).getUTF8String();
String serviceContextId = ccrAvps.getAvp(Avp.SERVICE_CONTEXT_ID).getUTF8String();
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' requested " + requestedUnits + " units for '" + serviceContextId + "'.");
}
Long balance = accounts.get(subscriptionId);
if (balance != null) {
if (balance <= 0) {
// DIAMETER_CREDIT_LIMIT_REACHED 4012
// The credit-control server denies the service request because the
// end user's account could not cover the requested service. If the
// CCR contained used-service-units they are deducted, if possible.
cca = createCCA(session, request, -1, 4012);
if (logger.isInfoEnabled()) {
logger.info("<> '" + subscriptionId + "' has insufficient credit units. Rejecting.");
}
}
else {
// Check if not first request, should have Used-Service-Unit AVP
if (ccrAvps.getAvp(Avp.CC_REQUEST_NUMBER) != null && ccrAvps.getAvp(Avp.CC_REQUEST_NUMBER).getUnsigned32() >= 1) {
Avp usedServiceUnit = ccrAvps.getAvp(Avp.USED_SERVICE_UNIT);
if (usedServiceUnit != null) {
Long wereReserved = reserved.remove(subscriptionId + "_" + serviceContextId);
wereReserved = wereReserved == null ? 0 : wereReserved;
long wereUsed = usedServiceUnit.getGrouped().getAvp(Avp.CC_TIME).getUnsigned32();
long remaining = wereReserved - wereUsed;
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' had " + wereReserved + " reserved units, " + wereUsed + " units were used."
+ " (rem: " + remaining + ").");
}
balance += remaining;
}
}
long grantedUnits = Math.min(requestedUnits, balance);
cca = createCCA(session, request, grantedUnits, ResultCode.SUCCESS);
reserved.put(subscriptionId + "_" + serviceContextId, grantedUnits);
balance -= grantedUnits;
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' Balance: " + (balance + grantedUnits) +
" // Available(" + balance + ") Reserved(" + grantedUnits + ")");
}
accounts.put(subscriptionId, balance);
// Check if the user has no more credit
if (balance <= 0) {
// 8.34. Final-Unit-Indication AVP
//
// The Final-Unit-Indication AVP (AVP Code 430) is of type Grouped and
// indicates that the Granted-Service-Unit AVP in the Credit-Control-
// Answer, or in the AA answer, contains the final units for the
// service. After these units have expired, the Diameter credit-control
// client is responsible for executing the action indicated in the
// Final-Unit-Action AVP (see section 5.6).
//
// If more than one unit type is received in the Credit-Control-Answer,
// the unit type that first expired SHOULD cause the credit-control
// client to execute the specified action.
//
// In the first interrogation, the Final-Unit-Indication AVP with
// Final-Unit-Action REDIRECT or RESTRICT_ACCESS can also be present
// with no Granted-Service-Unit AVP in the Credit-Control-Answer or in
// the AA answer. This indicates to the Diameter credit-control client
// to execute the specified action immediately. If the home service
// provider policy is to terminate the service, naturally, the server
// SHOULD return the appropriate transient failure (see section 9.1) in
// order to implement the policy-defined action.
//
// The Final-Unit-Action AVP defines the behavior of the service element
// when the user's account cannot cover the cost of the service and MUST
// always be present if the Final-Unit-Indication AVP is included in a
// command.
//
// If the Final-Unit-Action AVP is set to TERMINATE, no other AVPs MUST
// be present.
//
// If the Final-Unit-Action AVP is set to REDIRECT at least the
// Redirect-Server AVP MUST be present. The Restriction-Filter-Rule AVP
// or the Filter-Id AVP MAY be present in the Credit-Control-Answer
// message if the user is also allowed to access other services that are
// not accessible through the address given in the Redirect-Server AVP.
//
// If the Final-Unit-Action AVP is set to RESTRICT_ACCESS, either the
// Restriction-Filter-Rule AVP or the Filter-Id AVP SHOULD be present.
//
// The Filter-Id AVP is defined in [NASREQ]. The Filter-Id AVP can be
// used to reference an IP filter list installed in the access device by
// means other than the Diameter credit-control application, e.g.,
// locally configured or configured by another entity.
//
// The Final-Unit-Indication AVP is defined as follows (per the
// grouped-avp-def of RFC 3588 [DIAMBASE]):
//
// Final-Unit-Indication ::= < AVP Header: 430 >
// { Final-Unit-Action }
// *[ Restriction-Filter-Rule ]
// *[ Filter-Id ]
// [ Redirect-Server ]
AvpSet finalUnitIndicationAvp = cca.getMessage().getAvps().addGroupedAvp(Avp.FINAL_UNIT_INDICATION);
// 8.35. Final-Unit-Action AVP
//
// The Final-Unit-Action AVP (AVP Code 449) is of type Enumerated and
// indicates to the credit-control client the action to be taken when
// the user's account cannot cover the service cost.
//
// The Final-Unit-Action can be one of the following:
//
// TERMINATE 0
// The credit-control client MUST terminate the service session.
// This is the default handling, applicable whenever the credit-
// control client receives an unsupported Final-Unit-Action value,
// and it MUST be supported by all the Diameter credit-control client
// implementations conforming to this specification.
//
// REDIRECT 1
// The service element MUST redirect the user to the address
// specified in the Redirect-Server-Address AVP. The redirect action
// is defined in section 5.6.2.
//
// RESTRICT_ACCESS 2
// The access device MUST restrict the user access according to the
// IP packet filters defined in the Restriction-Filter-Rule AVP or
// according to the IP packet filters identified by the Filter-Id
// AVP. All the packets not matching the filters MUST be dropped
// (see section 5.6.3).
finalUnitIndicationAvp.addAvp(Avp.FINAL_UNIT_ACTION, 0);
}
}
}
else {
// DIAMETER_USER_UNKNOWN 5030
// The specified end user is unknown in the credit-control server.
cca = createCCA(session, request, -1, 5030);
cca.getMessage().setError(true);
if (logger.isInfoEnabled()) {
logger.info("<> '" + subscriptionId + "' is not provisioned in this server. Rejecting.");
}
}
//cca.getMessage().getAvps().addAvp(Avp.SERVICE_CONTEXT_ID, serviceContextId, false);
session.sendCreditControlAnswer(cca);
}
catch (Exception e) {
logger.error(">< Failure processing Credit-Control-Request [" + (request.getRequestTypeAVPValue() == 1 ? "INITIAL" : "UPDATE") + "]", e);
}
break;
// TERMINATION_REQUEST 3
case 3:
if (logger.isInfoEnabled()) {
logger.info("<< Received Credit-Control-Request [TERMINATION]");
}
try {
String subscriptionId = ccrAvps.getAvp(Avp.SUBSCRIPTION_ID).getGrouped().getAvp(Avp.SUBSCRIPTION_ID_DATA).getUTF8String();
String serviceContextId = ccrAvps.getAvp(Avp.SERVICE_CONTEXT_ID).getUTF8String();
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' requested service termination for '" + serviceContextId + "'.");
}
Long balance = accounts.get(subscriptionId);
if (ccrAvps.getAvp(Avp.CC_REQUEST_NUMBER) != null && ccrAvps.getAvp(Avp.CC_REQUEST_NUMBER).getUnsigned32() >= 1) {
Avp usedServiceUnit = ccrAvps.getAvp(Avp.USED_SERVICE_UNIT);
if (usedServiceUnit != null) {
long wereReserved = reserved.remove(subscriptionId + "_" + serviceContextId);
long wereUsed = usedServiceUnit.getGrouped().getAvp(Avp.CC_TIME).getUnsigned32();
long remaining = wereReserved - wereUsed;
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' had " + wereReserved + " reserved units, " + wereUsed + " units were used."
+ " (non-used: " + remaining + ").");
}
balance += remaining;
}
}
if (logger.isInfoEnabled()) {
logger.info(">> '" + subscriptionId + "' Balance: " + balance + " // Available(" + balance + ") Reserved(0)");
}
accounts.put(subscriptionId, balance);
cca = createCCA(session, request, -1, ResultCode.SUCCESS);
// 8.7. Cost-Information AVP
//
// The Cost-Information AVP (AVP Code 423) is of type Grouped, and it is
// used to return the cost information of a service, which the credit-
// control client can transfer transparently to the end user. The
// included Unit-Value AVP contains the cost estimate (always type of
// money) of the service, in the case of price enquiry, or the
// accumulated cost estimation, in the case of credit-control session.
//
// The Currency-Code specifies in which currency the cost was given.
// The Cost-Unit specifies the unit when the service cost is a cost per
// unit (e.g., cost for the service is $1 per minute).
//
// When the Requested-Action AVP with value PRICE_ENQUIRY is included in
// the Credit-Control-Request command, the Cost-Information AVP sent in
// the succeeding Credit-Control-Answer command contains the cost
// estimation of the requested service, without any reservation being
// made.
//
// The Cost-Information AVP included in the Credit-Control-Answer
// command with the CC-Request-Type set to UPDATE_REQUEST contains the
// accumulated cost estimation for the session, without taking any
// credit reservation into account.
//
// The Cost-Information AVP included in the Credit-Control-Answer
// command with the CC-Request-Type set to EVENT_REQUEST or
// TERMINATION_REQUEST contains the estimated total cost for the
// requested service.
//
// It is defined as follows (per the grouped-avp-def of
// RFC 3588 [DIAMBASE]):
//
// Cost-Information ::= < AVP Header: 423 >
// { Unit-Value }
// { Currency-Code }
// [ Cost-Unit ]
// 7.2.133 Remaining-Balance AVP
//
// The Remaining-Balance AVP (AVPcode 2021) is of type Grouped and
// provides information about the remaining account balance of the
// subscriber.
//
// It has the following ABNF grammar:
// Remaining-Balance :: = < AVP Header: 2021 >
// { Unit-Value }
// { Currency-Code }
// We use no money notion ... maybe later.
// AvpSet costInformation = ccaAvps.addGroupedAvp(423);
session.sendCreditControlAnswer(cca);
}
catch (Exception e) {
logger.error(">< Failure processing Credit-Control-Request [TERMINATION]", e);
}
break;
// EVENT_REQUEST 4
case 4:
if (logger.isInfoEnabled()) {
logger.info("<< Received Credit-Control-Request [EVENT]");
}
break;
default:
break;
}
}
@Override
public void doReAuthAnswer(ServerCCASession session, ReAuthRequest request, ReAuthAnswer answer) throws InternalException {
// Do Nothing.
}
@Override
public void sessionSupervisionTimerExpired(ServerCCASession session) {
// Do Nothing.
}
@Override
public void denyAccessOnTxExpire(ClientCCASession clientCCASessionImpl) {
// Do Nothing.
}
@Override
public void txTimerExpired(ClientCCASession session) {
// Do Nothing.
}
private JCreditControlAnswer createCCA(ServerCCASession session, JCreditControlRequest request, long grantedUnits, long resultCode)
throws InternalException, AvpDataException {
JCreditControlAnswerImpl answer = new JCreditControlAnswerImpl((Request) request.getMessage(), resultCode);
AvpSet ccrAvps = request.getMessage().getAvps();
AvpSet ccaAvps = answer.getMessage().getAvps();
// <Credit-Control-Answer> ::= < Diameter Header: 272, PXY >
// < Session-Id >
// { Result-Code }
// { Origin-Host }
// { Origin-Realm }
// { Auth-Application-Id }
// { CC-Request-Type }
// Using the same as the one present in request
ccaAvps.addAvp(ccrAvps.getAvp(Avp.CC_REQUEST_TYPE));
// { CC-Request-Number }
// Using the same as the one present in request
ccaAvps.addAvp(ccrAvps.getAvp(Avp.CC_REQUEST_NUMBER));
// [ User-Name ]
// [ CC-Session-Failover ]
// [ CC-Sub-Session-Id ]
// [ Acct-Multi-Session-Id ]
// [ Origin-State-Id ]
// [ Event-Timestamp ]
// [ Granted-Service-Unit ]
// 8.17. Granted-Service-Unit AVP
//
// Granted-Service-Unit AVP (AVP Code 431) is of type Grouped and
// contains the amount of units that the Diameter credit-control client
// can provide to the end user until the service must be released or the
// new Credit-Control-Request must be sent. A client is not required to
// implement all the unit types, and it must treat unknown or
// unsupported unit types in the answer message as an incorrect CCA
// answer. In this case, the client MUST terminate the credit-control
// session and indicate in the Termination-Cause AVP reason
// DIAMETER_BAD_ANSWER.
//
// The Granted-Service-Unit AVP is defined as follows (per the grouped-
// avp-def of RFC 3588 [DIAMBASE]):
//
// Granted-Service-Unit ::= < AVP Header: 431 >
// [ Tariff-Time-Change ]
// [ CC-Time ]
// [ CC-Money ]
// [ CC-Total-Octets ]
// [ CC-Input-Octets ]
// [ CC-Output-Octets ]
// [ CC-Service-Specific-Units ]
// *[ AVP ]
if (grantedUnits >= 0) {
AvpSet gsuAvp = ccaAvps.addGroupedAvp(Avp.GRANTED_SERVICE_UNIT);
// Fetch AVP/Value from Request
// gsuAvp.addAvp(ccrAvps.getAvp(Avp.REQUESTED_SERVICE_UNIT).getGrouped().getAvp(Avp.CC_TIME));
gsuAvp.addAvp(Avp.CC_TIME, grantedUnits, true);
}
// *[ Multiple-Services-Credit-Control ]
// [ Cost-Information]
// [ Final-Unit-Indication ]
// [ Check-Balance-Result ]
// [ Credit-Control-Failure-Handling ]
// [ Direct-Debiting-Failure-Handling ]
// [ Validity-Time]
// *[ Redirect-Host]
// [ Redirect-Host-Usage ]
// [ Redirect-Max-Cache-Time ]
// *[ Proxy-Info ]
// *[ Route-Record ]
// *[ Failed-AVP ]
// *[ AVP ]
if (logger.isInfoEnabled()) {
logger.info(">> Created Credit-Control-Answer.");
DiameterUtilities.printMessage(answer.getMessage());
}
return answer;
}
private static String readFile(InputStream is) throws IOException {
/*FileInputStream stream = new FileInputStream(is);
try {
FileChannel fc = stream.getChannel();
MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
// Instead of using default, pass in a decoder.
return Charset.defaultCharset().decode(bb).toString();
}
finally {
stream.close();
}*/
BufferedInputStream bin = new BufferedInputStream(is);
byte[] contents = new byte[1024];
int bytesRead = 0;
String strFileContents;
StringBuilder sb = new StringBuilder();
while ( (bytesRead = bin.read(contents)) != -1) {
strFileContents = new String(contents, 0, bytesRead);
sb.append(strFileContents);
}
return sb.toString();
}
}