/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2016, TeleStax Inc. and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* JBoss, Home of Professional Open Source
* Copyright 2007-2011, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jdiameter.client.impl.app.gq;
import static org.jdiameter.api.Message.SESSION_TERMINATION_REQUEST;
import static org.jdiameter.common.api.app.auth.ClientAuthSessionState.DISCONNECTED;
import static org.jdiameter.common.api.app.auth.ClientAuthSessionState.IDLE;
import static org.jdiameter.common.api.app.auth.ClientAuthSessionState.OPEN;
import static org.jdiameter.common.api.app.auth.ClientAuthSessionState.PENDING;
import java.io.Serializable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.jdiameter.api.Answer;
import org.jdiameter.api.Avp;
import org.jdiameter.api.AvpSet;
import org.jdiameter.api.EventListener;
import org.jdiameter.api.IllegalDiameterStateException;
import org.jdiameter.api.InternalException;
import org.jdiameter.api.NetworkReqListener;
import org.jdiameter.api.OverloadException;
import org.jdiameter.api.Request;
import org.jdiameter.api.RouteException;
import org.jdiameter.api.app.AppAnswerEvent;
import org.jdiameter.api.app.AppEvent;
import org.jdiameter.api.app.AppRequestEvent;
import org.jdiameter.api.app.AppSession;
import org.jdiameter.api.app.StateChangeListener;
import org.jdiameter.api.app.StateEvent;
import org.jdiameter.api.auth.ClientAuthSessionListener;
import org.jdiameter.api.auth.events.AbortSessionAnswer;
import org.jdiameter.api.auth.events.AbortSessionRequest;
import org.jdiameter.api.auth.events.ReAuthAnswer;
import org.jdiameter.api.auth.events.ReAuthRequest;
import org.jdiameter.api.auth.events.SessionTermAnswer;
import org.jdiameter.api.auth.events.SessionTermRequest;
import org.jdiameter.api.gq.GqClientSession;
import org.jdiameter.client.api.ISessionFactory;
import org.jdiameter.client.impl.app.auth.IClientAuthSessionData;
import org.jdiameter.common.api.app.IAppSessionState;
import org.jdiameter.common.api.app.auth.ClientAuthSessionState;
import org.jdiameter.common.api.app.auth.IAuthMessageFactory;
import org.jdiameter.common.api.app.auth.IClientAuthActionContext;
import org.jdiameter.common.impl.app.AppAnswerEventImpl;
import org.jdiameter.common.impl.app.AppRequestEventImpl;
import org.jdiameter.common.impl.app.auth.AbortSessionAnswerImpl;
import org.jdiameter.common.impl.app.auth.AbortSessionRequestImpl;
import org.jdiameter.common.impl.app.auth.AppAuthSessionImpl;
import org.jdiameter.common.impl.app.auth.ReAuthAnswerImpl;
import org.jdiameter.common.impl.app.auth.ReAuthRequestImpl;
import org.jdiameter.common.impl.app.auth.SessionTermAnswerImpl;
import org.jdiameter.common.impl.app.auth.SessionTermRequestImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Client Gq Application session implementation
*
* @author <a href="mailto:webdev@web-ukraine.info"> Yulian Oifa </a>
* @author <a href="mailto:brainslog@gmail.com"> Alexandre Mendonca </a>
* @author <a href="mailto:baranowb@gmail.com"> Bartosz Baranowski </a>
*/
public class GqClientSessionImpl extends AppAuthSessionImpl implements GqClientSession, EventListener<Request, Answer>, NetworkReqListener {
protected static final Logger logger = LoggerFactory.getLogger(GqClientSessionImpl.class);
// Session State Handling ---------------------------------------------------
protected Lock sendAndStateLock = new ReentrantLock();
// Factories and Listeners --------------------------------------------------
protected transient IAuthMessageFactory factory;
protected transient IClientAuthActionContext context;
protected transient ClientAuthSessionListener listener;
protected static final String TIMER_NAME_TS = "GQ_TS";
protected IClientAuthSessionData sessionData;
// Constructors -------------------------------------------------------------
public GqClientSessionImpl(IClientAuthSessionData sessionData, ISessionFactory sf, ClientAuthSessionListener lst, IAuthMessageFactory fct,
StateChangeListener<AppSession> scListener, IClientAuthActionContext context, boolean stateless) {
super(sf, sessionData);
if (lst == null) {
throw new IllegalArgumentException("Listener can not be null");
}
if (fct.getApplicationId() == null) {
throw new IllegalArgumentException("ApplicationId can not be null");
}
super.appId = fct.getApplicationId();
this.listener = lst;
this.factory = fct;
this.context = context;
this.sessionData = sessionData;
this.sessionData.setStateless(stateless);
super.addStateChangeNotification(scListener);
}
// ClientAuthSession Implementation methods ---------------------------------
@Override
public void sendAbortSessionAnswer(AbortSessionAnswer answer) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
send(Event.Type.SEND_SESSION_ABORT_ANSWER, answer);
}
@Override
public void sendAuthRequest(AppRequestEvent request) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
send(Event.Type.SEND_AUTH_REQUEST, request);
}
@Override
public void sendReAuthAnswer(ReAuthAnswer answer) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
send(Event.Type.SEND_AUTH_ANSWER, answer);
}
@Override
public void sendSessionTerminationRequest(SessionTermRequest request)
throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
send(Event.Type.SEND_SESSION_TERMINATION_REQUEST, request);
}
protected void send(Event.Type type, AppEvent event) throws InternalException {
//This is called from app thread, it may be due to callback from our delivery thread, but we dont care
try {
sendAndStateLock.lock();
if (type != null) {
handleEvent(new Event(type, event));
}
session.send(event.getMessage(), this);
// Store last destination information
AvpSet avps = event.getMessage().getAvps();
Avp destRealmAvp = avps.getAvp(Avp.DESTINATION_REALM);
if (destRealmAvp != null) {
sessionData.setDestinationRealm(destRealmAvp.getDiameterIdentity());
}
Avp destHostAvp = avps.getAvp(Avp.DESTINATION_HOST);
if (destHostAvp != null) {
sessionData.setDestinationHost(destHostAvp.getDiameterIdentity());
}
}
catch (Exception e) {
throw new InternalException(e);
}
finally {
sendAndStateLock.unlock();
}
}
@Override
public boolean isStateless() {
return this.sessionData.isStateless();
}
@SuppressWarnings("unchecked")
protected void setState(ClientAuthSessionState newState) {
IAppSessionState oldState = sessionData.getClientAuthSessionState();
sessionData.setClientAuthSessionState(newState);
for (StateChangeListener i : stateListeners) {
i.stateChanged(this,(Enum) oldState, (Enum) newState);
}
}
@Override
@SuppressWarnings("unchecked")
public <E> E getState(Class<E> eClass) {
return eClass == ClientAuthSessionState.class ? (E) sessionData.getClientAuthSessionState() : null;
}
@Override
public boolean handleEvent(StateEvent event) throws InternalException, OverloadException {
return sessionData.isStateless() ? handleEventForStatelessSession(event) : handleEventForStatefulSession(event);
}
public boolean handleEventForStatelessSession(StateEvent event) throws InternalException, OverloadException {
try {
ClientAuthSessionState state = sessionData.getClientAuthSessionState();
ClientAuthSessionState oldState = state;
switch (state) {
case IDLE:
switch ((Event.Type) event.getType()) {
case SEND_AUTH_REQUEST:
// Current State: IDLE
// Event: Client or Device Requests access
// Action: Send service specific auth req
// New State: PENDING
setState(PENDING);
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
case PENDING:
switch ((Event.Type) event.getType()) {
case RECEIVE_AUTH_ANSWER:
try {
// Current State: PENDING
// Event: Successful service-specific authorization answer received with Auth-Session-State set to NO_STATE_MAINTAINED
// Action: Grant Access
// New State: OPEN
listener.doAuthAnswerEvent(this, null, (AppAnswerEvent) event.getData());
setState(OPEN);
}
catch (Exception e) {
// Current State: PENDING
// Event: Failed service-specific authorization answer received
// Action: Cleanup
// New State: IDLE
setState(IDLE);
}
break;
case SEND_SESSION_TERMINATION_REQUEST:
// Current State: OPEN
// Event: Service to user is terminated
// Action: Disconnect User/Device
// New State: IDLE
setState(IDLE);
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
case OPEN:
switch ((Event.Type) event.getType()) {
case SEND_SESSION_ABORT_ANSWER:
case SEND_SESSION_TERMINATION_REQUEST:
// Current State: OPEN
// Event: Service to user is terminated
// Action: Disconnect User/Device
// New State: IDLE
setState(IDLE);
break;
case TIMEOUT_EXPIRES:
// Current State: OPEN
// Event: Session-Timeout Expires on Access Device
// Action: Send STR
// New State: DISCON
if (context != null) {
context.accessTimeoutElapses(this);
Request str = createSessionTermRequest();
context.disconnectUserOrDev(this, str);
session.send(str, this);
}
// IDLE is the same as DISCON
setState(IDLE);
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
}
// post processing
if (oldState != state) {
if (DISCONNECTED.equals(state) || IDLE.equals(state)) {
cancelTsTimer();
}
else if (OPEN.equals(state) && context != null && context.getAccessTimeout() > 0) {
cancelTsTimer();
startTsTimer();
}
}
}
catch (Throwable t) {
throw new InternalException(t);
}
return true;
}
public boolean handleEventForStatefulSession(StateEvent event) throws InternalException, OverloadException {
ClientAuthSessionState state = sessionData.getClientAuthSessionState();
ClientAuthSessionState oldState = state;
try {
switch (state) {
case IDLE: {
switch ((Event.Type) event.getType()) {
case SEND_AUTH_REQUEST:
// Current State: IDLE
// Event: Client or Device Requests access
// Action: Send service specific auth req
// New State: PENDING
setState(PENDING);
break;
case RECEIVE_ABORT_SESSION_REQUEST:
// Current State: IDLE
// Event: ASR Received for unknown session
// Action: Send ASA with Result-Code = UNKNOWN_SESSION_ID
// New State: IDLE
// FIXME: Should send ASA with UNKNOWN_SESSION_ID instead ?
listener.doAbortSessionRequestEvent(this, (AbortSessionRequest) event.getData());
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
}
case PENDING: {
switch ((Event.Type) event.getType()) {
case RECEIVE_AUTH_ANSWER:
try {
// Current State: PENDING
// Event: Successful service-specific authorization answer received with default Auth-Session-State value
// Action: Grant Access
// New State: OPEN
listener.doAuthAnswerEvent(this, null, (AppAnswerEvent) event.getData());
setState(OPEN);
}
catch (InternalException e) {
// Current State: PENDING
// Event: Successful service-specific authorization answer received but service not provided
// Action: Send STR
// New State: DISCON
// Current State: PENDING
// Event: Error Processing successful service-specific authorization answer
// Action: Send STR
// New State: DISCON
setState(DISCONNECTED);
}
catch (Exception e) {
// Current State: PENDING
// Event: Failed service-specific authorization answer received
// Action: Cleanup
// New State: IDLE
setState(IDLE);
}
break;
case SEND_SESSION_TERMINATION_REQUEST:
// Current State: OPEN
// Event: Service to user is terminated
// Action: Disconnect User/Device
// New State: IDLE
setState(DISCONNECTED);
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
}
case OPEN: {
switch ((Event.Type) event.getType()) {
case SEND_AUTH_REQUEST:
// Current State: OPEN
// Event: User or client device requests access to service
// Action: Send service specific auth req
// New State: OPEN
break;
case RECEIVE_AUTH_ANSWER:
try {
// Current State: OPEN
// Event: Successful service-specific authorization answer received
// Action: Provide Service
// New State: OPEN
listener.doAuthAnswerEvent(this, null, (AppAnswerEvent) event.getData());
}
catch (Exception e) {
// Current State: OPEN
// Event: ASR Received, client will comply with request to end the session
// Action: Send ASA with Result-Code = SUCCESS, Send STR
// New State: DISCON
setState(DISCONNECTED);
}
break;
case RECEIVE_FAILED_AUTH_ANSWER:
// Current State: OPEN
// Event: Failed Service-specific authorization answer received
// Action: Disconnect User/Device
// New State: IDLE
if (context != null) {
Request str = createSessionTermRequest();
context.disconnectUserOrDev(this, str);
session.send(str, this);
}
setState(IDLE);
break;
case RECEIVE_ABORT_SESSION_REQUEST:
// Current State: OPEN
// Event: ASR Received (client to take comply or not)
// Action: TBD
// New State: TBD (comply = DISCON, !comply = OPEN)
listener.doAbortSessionRequestEvent(this, (AbortSessionRequestImpl) event.getData());
break;
case SEND_SESSION_TERMINATION_REQUEST:
setState(DISCONNECTED);
break;
case TIMEOUT_EXPIRES:
// Current State: OPEN
// Event: Session-Timeout Expires on Access Device
// Action: Send STR
// New State: DISCON
// Current State: OPEN
// Event: Authorization-Lifetime + Auth-Grace-Period expires on access device
// Action: Send STR
// New State: DISCON
if (context != null) {
context.accessTimeoutElapses(this);
Request str = createSessionTermRequest();
context.disconnectUserOrDev(this, str);
session.send(str, this);
}
setState(DISCONNECTED);
break;
}
break;
}
case DISCONNECTED: {
switch ((Event.Type) event.getType()) {
case RECEIVE_ABORT_SESSION_REQUEST:
// Current State: DISCON
// Event: ASR Received
// Action: Send ASA
// New State: DISCON
listener.doAbortSessionRequestEvent(this, (AbortSessionRequest) event.getData());
break;
case RECEIVE_SESSION_TERINATION_ANSWER:
// Current State: DISCON
// Event: STA Received
// Action: Disconnect User/Device
// New State: IDLE
listener.doSessionTerminationAnswerEvent(this, ((SessionTermAnswerImpl) event.getData()));
setState(IDLE);
break;
default:
logger.debug("Unknown event [{}]", event.getType());
break;
}
break;
}
default: {
logger.debug("Unknown state [{}]", state);
break;
}
}
// post processing
if (oldState != state) {
if (OPEN.equals(state) && context != null && context.getAccessTimeout() > 0) {
cancelTsTimer();
startTsTimer();
}
}
}
catch (Throwable t) {
throw new InternalException(t);
}
return true;
}
@Override
public void receivedSuccessMessage(Request request, Answer answer) {
AnswerDelivery ad = new AnswerDelivery();
ad.session = this;
ad.request = request;
ad.answer = answer;
super.scheduler.execute(ad);
}
@Override
public void timeoutExpired(Request request) {
try {
//FIXME: should this also be async ?
handleEvent(new Event(Event.Type.RECEIVE_FAILED_AUTH_ANSWER, new AppRequestEventImpl(request)));
}
catch (Exception e) {
logger.debug("Can not handle timeout event", e);
}
}
@Override
public Answer processRequest(Request request) {
RequestDelivery rd = new RequestDelivery();
rd.session = this;
rd.request = request;
super.scheduler.execute(rd);
return null;
}
/* (non-Javadoc)
* @see org.jdiameter.common.impl.app.AppSessionImpl#isReplicable()
*/
@Override
public boolean isReplicable() {
return true;
}
protected void startTsTimer() throws IllegalArgumentException, InternalException {
try {
sendAndStateLock.lock();
sessionData.setTsTimerId(super.timerFacility.schedule(sessionData.getSessionId(), TIMER_NAME_TS, context.getAccessTimeout()));
}
finally {
sendAndStateLock.unlock();
}
}
protected void cancelTsTimer() {
try {
sendAndStateLock.lock();
Serializable timerId = sessionData.getTsTimerId();
if (timerId != null) {
super.timerFacility.cancel(timerId);
sessionData.setTsTimerId(null);
}
}
finally {
sendAndStateLock.unlock();
}
}
/* (non-Javadoc)
* @see org.jdiameter.common.impl.app.AppSessionImpl#onTimer(java.lang.String)
*/
@Override
public void onTimer(String timerName) {
if (timerName.equals(IDLE_SESSION_TIMER_NAME)) {
checkIdleAppSession();
}
else if (timerName.equals(TIMER_NAME_TS)) {
try {
sendAndStateLock.lock();
sessionData.setTsTimerId(null);
if (context != null) {
try {
handleEvent(new Event(Event.Type.TIMEOUT_EXPIRES, null));
}
catch (Exception e) {
logger.debug("Can not handle event", e);
}
}
}
finally {
sendAndStateLock.unlock();
}
}
else {
logger.warn("Received an unknown timer '{}' for Session-ID '{}'", timerName, getSessionId());
}
}
protected AbortSessionAnswer createAbortSessionAnswer(Answer answer) {
return new AbortSessionAnswerImpl(answer);
}
protected AbortSessionRequest createAbortSessionRequest(Request request) {
return new AbortSessionRequestImpl(request);
}
protected ReAuthAnswer createReAuthAnswer(Answer answer) {
return new ReAuthAnswerImpl(answer);
}
protected ReAuthRequest createReAuthRequest(Request request) {
return new ReAuthRequestImpl(request);
}
protected SessionTermAnswer createSessionTermAnswer(Answer answer) {
return new SessionTermAnswerImpl(answer);
}
protected SessionTermRequest createSessionTermRequest(Request request) {
return new SessionTermRequestImpl(request);
}
protected Request createSessionTermRequest() {
return session.createRequest(SESSION_TERMINATION_REQUEST, appId, sessionData.getDestinationRealm(), sessionData.getDestinationHost());
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((sessionData == null) ? 0 : sessionData.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
GqClientSessionImpl other = (GqClientSessionImpl) obj;
if (sessionData == null) {
if (other.sessionData != null) {
return false;
}
}
else if (!sessionData.equals(other.sessionData)) {
return false;
}
return true;
}
private class RequestDelivery implements Runnable {
GqClientSession session;
Request request;
@Override
public void run() {
try {
if (request.getCommandCode() == AbortSessionRequest.code) {
handleEvent(new Event(Event.Type.RECEIVE_ABORT_SESSION_REQUEST, createAbortSessionRequest(request)));
}
else if (request.getCommandCode() == ReAuthRequest.code) {
listener.doReAuthRequestEvent(session, createReAuthRequest(request));
}
else {
listener.doOtherEvent(session, factory.createAuthRequest(request), null);
}
}
catch (Exception e) {
logger.debug("Can not process received request", e);
}
}
}
private class AnswerDelivery implements Runnable {
GqClientSession session;
Answer answer;
Request request;
@Override
public void run() {
try {
sendAndStateLock.lock();
// FIXME: baranowb: this shouldn't be like that?
if (answer.getCommandCode() == factory.getAuthMessageCommandCode()) {
handleEvent(new Event(Event.Type.RECEIVE_AUTH_ANSWER, factory.createAuthAnswer(answer)));
}
else if (answer.getCommandCode() == SessionTermAnswer.code) {
handleEvent(new Event(Event.Type.RECEIVE_SESSION_TERINATION_ANSWER, createSessionTermAnswer(answer)));
}
else {
listener.doOtherEvent(session, factory.createAuthRequest(request), new AppAnswerEventImpl(answer));
}
}
catch (Exception e) {
logger.debug("Can not process received message", e);
}
finally {
sendAndStateLock.unlock();
}
}
}
}