/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.apache.vysper.xmpp.delivery.inbound; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.vysper.compliance.SpecCompliant; import org.apache.vysper.storage.StorageProviderRegistry; import org.apache.vysper.xmpp.addressing.Entity; import org.apache.vysper.xmpp.addressing.EntityUtils; import org.apache.vysper.xmpp.authentication.AccountManagement; import org.apache.vysper.xmpp.delivery.OfflineStanzaReceiver; import org.apache.vysper.xmpp.delivery.StanzaRelay; import org.apache.vysper.xmpp.delivery.failure.DeliveredToOfflineReceiverException; import org.apache.vysper.xmpp.delivery.failure.DeliveryException; import org.apache.vysper.xmpp.delivery.failure.DeliveryFailureStrategy; import org.apache.vysper.xmpp.delivery.failure.LocalRecipientOfflineException; import org.apache.vysper.xmpp.delivery.failure.NoSuchLocalUserException; import org.apache.vysper.xmpp.delivery.failure.ServiceNotAvailableException; import org.apache.vysper.xmpp.modules.extension.xep0160_offline_storage.OfflineStorageProvider; import org.apache.vysper.xmpp.protocol.SessionStateHolder; import org.apache.vysper.xmpp.protocol.StanzaHandler; import org.apache.vysper.xmpp.protocol.StanzaProcessor; import org.apache.vysper.xmpp.protocol.worker.InboundStanzaProtocolWorker; import org.apache.vysper.xmpp.server.ServerRuntimeContext; import org.apache.vysper.xmpp.server.SessionContext; import org.apache.vysper.xmpp.server.SessionState; import org.apache.vysper.xmpp.stanza.IQStanza; import org.apache.vysper.xmpp.stanza.MessageStanza; import org.apache.vysper.xmpp.stanza.MessageStanzaType; import org.apache.vysper.xmpp.stanza.PresenceStanza; import org.apache.vysper.xmpp.stanza.Stanza; import org.apache.vysper.xmpp.stanza.XMPPCoreStanza; import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * relays all 'incoming' stanzas to internal sessions, acts as a 'stage' by using a ThreadPoolExecutor * 'incoming' here means: * a. stanzas coming in from other servers * b. stanzas coming from other (local) sessions and are targeted to clients on this server * * @author The Apache MINA Project (dev@mina.apache.org) */ public class DeliveringInternalInboundStanzaRelay implements StanzaRelay { final Logger logger = LoggerFactory.getLogger(DeliveringInternalInboundStanzaRelay.class); private static final InboundStanzaProtocolWorker INBOUND_STANZA_PROTOCOL_WORKER = new InboundStanzaProtocolWorker(); private static final Integer PRIO_THRESHOLD = 0; protected ResourceRegistry resourceRegistry; protected ExecutorService executor; protected AccountManagement accountVerification; protected OfflineStanzaReceiver offlineStanzaReceiver = null; protected Entity serverEntity; protected ServerRuntimeContext serverRuntimeContext = null; public DeliveringInternalInboundStanzaRelay(Entity serverEntity, ResourceRegistry resourceRegistry, StorageProviderRegistry storageProviderRegistry) { this(serverEntity, resourceRegistry, (AccountManagement) storageProviderRegistry .retrieve(AccountManagement.class),(OfflineStanzaReceiver)storageProviderRegistry.retrieve(OfflineStorageProvider.class)); } public DeliveringInternalInboundStanzaRelay(Entity serverEntity, ResourceRegistry resourceRegistry, AccountManagement accountVerification, OfflineStanzaReceiver offlineStanzaReceiver) { this.serverEntity = serverEntity; this.resourceRegistry = resourceRegistry; this.accountVerification = accountVerification; this.offlineStanzaReceiver =offlineStanzaReceiver; int coreThreadCount = 10; int maxThreadCount = 20; int threadTimeoutSeconds = 2 * 60 * 1000; this.executor = new ThreadPoolExecutor(coreThreadCount, maxThreadCount, threadTimeoutSeconds, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); } /*package*/ DeliveringInternalInboundStanzaRelay(ExecutorService executor) { this.executor = executor; } public void setServerRuntimeContext(ServerRuntimeContext serverRuntimeContext) { this.serverRuntimeContext = serverRuntimeContext; } public void relay(Entity receiver, Stanza stanza, DeliveryFailureStrategy deliveryFailureStrategy) throws DeliveryException { if (!isRelaying()) { throw new ServiceNotAvailableException("internal inbound relay is not relaying"); } Future<RelayResult> resultFuture = executor.submit(new Relay(receiver, stanza, deliveryFailureStrategy)); } public boolean isRelaying() { return !executor.isShutdown(); } public void stop() { this.executor.shutdown(); } private class Relay implements Callable<RelayResult> { private Entity receiver; private Stanza stanza; private DeliveryFailureStrategy deliveryFailureStrategy; protected final UnmodifyableSessionStateHolder sessionStateHolder = new UnmodifyableSessionStateHolder(); Relay(Entity receiver, Stanza stanza, DeliveryFailureStrategy deliveryFailureStrategy) { this.receiver = receiver; this.stanza = stanza; this.deliveryFailureStrategy = deliveryFailureStrategy; } public Entity getReceiver() { return receiver; } public Stanza getStanza() { return stanza; } public DeliveryFailureStrategy getDeliveryFailureStrategy() { return deliveryFailureStrategy; } public RelayResult call() { RelayResult relayResult = deliver(); if (relayResult == null || !relayResult.hasProcessingErrors()) return relayResult; return runFailureStrategy(relayResult); } private RelayResult runFailureStrategy(RelayResult relayResult) { if (deliveryFailureStrategy != null) { try { deliveryFailureStrategy.process(stanza, relayResult.getProcessingErrors()); } catch (DeliveryException e) { return new RelayResult(e); } catch (RuntimeException e) { return new RelayResult(new DeliveryException(e)); } } // TODO throw relayResult.getProcessingError() in some appropriate context return relayResult; } /** * @return */ @SpecCompliant(spec = "draft-ietf-xmpp-3921bis-00", section = "8.", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.COMPLETE) protected RelayResult deliver() { try { String receiverDomain = receiver.getDomain(); if (receiverDomain != null && !EntityUtils.isAddressingServer(receiver, serverEntity)) { if (serverRuntimeContext == null) { return new RelayResult(new ServiceNotAvailableException( "cannot retrieve component from server context")); } if (!EntityUtils.isAddressingServerComponent(receiver, serverEntity)) { return new RelayResult(new ServiceNotAvailableException("unsupported domain " + receiverDomain)); } StanzaProcessor processor = serverRuntimeContext.getComponentStanzaProcessor(receiver); if (processor == null) { return new RelayResult(new ServiceNotAvailableException( "cannot retrieve component stanza processor for" + receiverDomain)); } processor.processStanza(serverRuntimeContext, null, stanza, null); return new RelayResult(); } if (receiver.isResourceSet()) { return deliverToFullJID(); } else { return deliverToBareJID(); } } catch (RuntimeException e) { return new RelayResult(new DeliveryException(e)); } } @SpecCompliant(spec = "draft-ietf-xmpp-3921bis-00", section = "8.3.", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.COMPLETE) private RelayResult deliverToBareJID() { XMPPCoreStanza xmppStanza = XMPPCoreStanza.getWrapper(stanza); if (xmppStanza == null) return new RelayResult(new DeliveryException( "unable to deliver stanza which is not IQ, presence or message")); if (PresenceStanza.isOfType(stanza)) { return relayToAllSessions(); } else if (MessageStanza.isOfType(stanza)) { MessageStanza messageStanza = (MessageStanza) xmppStanza; MessageStanzaType messageStanzaType = messageStanza.getMessageType(); switch (messageStanzaType) { case CHAT: case NORMAL: return serverRuntimeContext.getServerFeatures().isDeliveringMessageToHighestPriorityResourcesOnly() ? relayToBestSessions(false) : relayToAllSessions(0); case ERROR: // silently ignore return null; case GROUPCHAT: return new RelayResult(new ServiceNotAvailableException()); case HEADLINE: return relayToAllSessions(); default: throw new RuntimeException("unhandled message type " + messageStanzaType.value()); } } else if (IQStanza.isOfType(stanza)) { // TODO handle on behalf of the user/client return relayToBestSessions(false); } return relayNotPossible(); } @SpecCompliant(spec = "draft-ietf-xmpp-3921bis-00", section = "8.2.", status = SpecCompliant.ComplianceStatus.IN_PROGRESS, coverage = SpecCompliant.ComplianceCoverage.COMPLETE) private RelayResult deliverToFullJID() { XMPPCoreStanza xmppStanza = XMPPCoreStanza.getWrapper(stanza); if (xmppStanza == null) new RelayResult(new DeliveryException("unable to deliver stanza which is not IQ, presence or message")); // all special cases are handled by the inbound handlers! if (PresenceStanza.isOfType(stanza)) { // TODO cannot deliver presence with type AVAIL or UNAVAIL: silently ignore // TODO cannot deliver presence with type SUBSCRIBE: see 3921bis section 3.1.3 // TODO cannot deliver presence with type (UN)SUBSCRIBED, UNSUBSCRIBE: silently ignore return relayToBestSessions(false); } else if (MessageStanza.isOfType(stanza)) { MessageStanza messageStanza = (MessageStanza) xmppStanza; MessageStanzaType messageStanzaType = messageStanza.getMessageType(); boolean fallbackToBareJIDAllowed = messageStanzaType == MessageStanzaType.CHAT || messageStanzaType == MessageStanzaType.HEADLINE || messageStanzaType == MessageStanzaType.NORMAL; // TODO cannot deliver ERROR: silently ignore // TODO cannot deliver GROUPCHAT: service n/a return relayToBestSessions(fallbackToBareJIDAllowed); } else if (IQStanza.isOfType(stanza)) { // TODO no resource matches: service n/a return relayToBestSessions(false); } // for any other type of stanza return new RelayResult(new ServiceNotAvailableException()); } private RelayResult relayNotPossible() { if (!accountVerification.verifyAccountExists(receiver)) { logger.warn("cannot relay to unexisting receiver {} stanza {}", receiver.getFullQualifiedName(), stanza .toString()); return new RelayResult(new NoSuchLocalUserException()); } else if (offlineStanzaReceiver != null) { offlineStanzaReceiver.receive(stanza); return new RelayResult(new DeliveredToOfflineReceiverException()); } else { logger.warn("cannot relay to offline receiver {} stanza {}", receiver.getFullQualifiedName(), stanza .toString()); return new RelayResult(new LocalRecipientOfflineException()); } } protected RelayResult relayToBestSessions(final boolean fallbackToBareJIDAllowed) { List<SessionContext> receivingSessions = resourceRegistry.getHighestPrioSessions(receiver, PRIO_THRESHOLD); if (receivingSessions.size() == 0 && receiver.isResourceSet() && fallbackToBareJIDAllowed) { // no concrete session for this resource has been found // fall back to bare JID receivingSessions = resourceRegistry.getHighestPrioSessions(receiver.getBareJID(), PRIO_THRESHOLD); } if (receivingSessions.size() == 0) { return relayNotPossible(); } RelayResult relayResult = new RelayResult(); for (SessionContext receivingSession : receivingSessions) { if (receivingSession.getState() != SessionState.AUTHENTICATED) { relayResult.addProcessingError(new DeliveryException("no relay to non-authenticated sessions")); continue; } try { StanzaHandler stanzaHandler = receivingSession.getServerRuntimeContext().getHandler(stanza); INBOUND_STANZA_PROTOCOL_WORKER.processStanza(receivingSession, sessionStateHolder, stanza, stanzaHandler); } catch (Exception e) { relayResult.addProcessingError(new DeliveryException("no relay to non-authenticated sessions")); continue; } } return relayResult; } protected RelayResult relayToAllSessions() { return relayToAllSessions(null); } protected RelayResult relayToAllSessions(Integer prioThreshold) { List<SessionContext> receivingSessions = prioThreshold == null ? resourceRegistry.getSessions(receiver) : resourceRegistry.getSessions(receiver, prioThreshold); if (receivingSessions.size() == 0) { return relayNotPossible(); } if (receivingSessions.size() > 1) { logger.warn("multiplexing: {} sessions will be processing {} ", receivingSessions.size(), stanza); } RelayResult relayResult = new RelayResult(); for (SessionContext sessionContext : receivingSessions) { if (sessionContext.getState() != SessionState.AUTHENTICATED) { relayResult.addProcessingError(new DeliveryException("no relay to non-authenticated sessions")); continue; } try { StanzaHandler stanzaHandler = sessionContext.getServerRuntimeContext().getHandler(stanza); INBOUND_STANZA_PROTOCOL_WORKER.processStanza(sessionContext, sessionStateHolder, stanza, stanzaHandler); } catch (Exception e) { relayResult.addProcessingError(new DeliveryException(e)); } } return relayResult; // return success result } } private static class UnmodifyableSessionStateHolder extends SessionStateHolder { @Override public void setState(SessionState newState) { throw new RuntimeException("unable to alter state"); } @Override public SessionState getState() { return SessionState.AUTHENTICATED; } } }