/* * 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.extension.xep0124; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletRequest; import org.apache.vysper.xml.fragment.Attribute; import org.apache.vysper.xml.fragment.XMLElement; import org.apache.vysper.xmpp.protocol.NamespaceURIs; import org.apache.vysper.xmpp.server.ServerRuntimeContext; import org.apache.vysper.xmpp.server.SessionState; import org.apache.vysper.xmpp.server.response.ServerResponses; import org.apache.vysper.xmpp.stanza.Stanza; import org.apache.vysper.xmpp.stanza.StanzaBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Processes the BOSH requests from the clients * <p> * This class is thread safe, it handles concurrent BOSH requests safely. * * @author The Apache MINA Project (dev@mina.apache.org) */ public class BoshHandler { private static final Logger LOGGER = LoggerFactory.getLogger(BoshHandler.class); private ServerRuntimeContext serverRuntimeContext; private Map<String, BoshBackedSessionContext> sessions; private InactivityChecker inactivityChecker; public BoshHandler() { // The sessions are stored in a ConcurrentHashMap to maintain the "happens before relationship" memory consistency. // Although the operations on specific sessions are synchronized, the session creation and retrieval need the memory // consistency guarantee too. sessions = new ConcurrentHashMap<String, BoshBackedSessionContext>(); inactivityChecker = new InactivityChecker(); inactivityChecker.start(); } /** * Getting for the {@link ServerRuntimeContext} * @return The current {@link ServerRuntimeContext} */ public ServerRuntimeContext getServerRuntimeContext() { return serverRuntimeContext; } /** * Setter for the {@link ServerRuntimeContext} * @param serverRuntimeContext */ public void setServerRuntimeContext(ServerRuntimeContext serverRuntimeContext) { this.serverRuntimeContext = serverRuntimeContext; } /** * Processes BOSH requests * @param httpRequest the HTTP request * @param body the decoded BOSH request */ public void process(HttpServletRequest httpRequest, Stanza body) { if (!body.getNamespaceURI().equalsIgnoreCase(NamespaceURIs.XEP0124_BOSH)) { LOGGER.error("Invalid namespace for body wrapper '{}', must be '{}'!", body.getNamespaceURI(), NamespaceURIs.XEP0124_BOSH); return; } if (!body.getName().equalsIgnoreCase("body")) { LOGGER.error("Invalid body wrapper '{}'!", body.getName()); return; } if (body.getAttribute("rid") == null) { LOGGER.error("Invalid request that does not have a request identifier (rid) attribute!"); return; } BoshRequest br = new BoshRequest(httpRequest, body, Long.parseLong(body.getAttributeValue("rid"))); if (body.getAttribute("sid") == null) { // the session creation request (first request) does not have a "sid" attribute try { createSession(br); } catch (IOException e) { LOGGER.error("Exception thrown while processing the session creation request", e); return; } } else { BoshBackedSessionContext session = sessions.get(body.getAttributeValue("sid")); if (session == null) { LOGGER.warn("Received an invalid 'sid'!"); return; } synchronized (session) { session.insertRequest(br); for (;;) { // When a request from the user comes in, it is possible that the request fills a gap // created by previous lost request, and it could be possible to process more than the current request // continuing with all the adjacent requests. br = session.getNextRequest(); if (br == null) { break; } processSession(session, br); } } } } private void processSession(BoshBackedSessionContext session, BoshRequest br) { if (session.getState() == SessionState.ENCRYPTED) { if (br.getBody().getInnerElements().isEmpty()) { // session needs authentication return; } for (XMLElement element : br.getBody().getInnerElements()) { if (element.getNamespaceURI().equals(NamespaceURIs.URN_IETF_PARAMS_XML_NS_XMPP_SASL)) { processStanza(session, element); } } } else if (session.getState() == SessionState.AUTHENTICATED) { if ("true".equals(br.getBody().getAttributeValue(NamespaceURIs.URN_XMPP_XBOSH, "restart"))) { // restart request session.write0(getRestartResponse()); } else { // any other request for (XMLElement element : br.getBody().getInnerElements()) { processStanza(session, element); } // if the client solicited the session termination if ("terminate".equals(br.getBody().getAttributeValue("type"))) { terminateSession(session); } } } } private void terminateSession(BoshBackedSessionContext session) { sessions.remove(session.getSessionId()); session.write0(getTerminateResponse()); session.close(); } private void processStanza(BoshBackedSessionContext session, XMLElement element) { Stanza stanza; if (element instanceof Stanza) { stanza = (Stanza) element; } else { stanza = new Stanza(element.getNamespaceURI(), element.getName(), element.getNamespacePrefix(), element.getAttributes(), element.getInnerFragments()); } serverRuntimeContext.getStanzaProcessor().processStanza(serverRuntimeContext, session, stanza, session.getStateHolder()); } private void createSession(BoshRequest br) throws IOException { BoshBackedSessionContext session = new BoshBackedSessionContext(this, serverRuntimeContext, inactivityChecker); if (br.getBody().getAttribute("content") != null) { session.setContentType(br.getBody().getAttributeValue("content")); } if (br.getBody().getAttribute("wait") != null) { int wait = Integer.parseInt(br.getBody().getAttributeValue("wait")); session.setWait(wait); } if (br.getBody().getAttribute("hold") != null) { int hold = Integer.parseInt(br.getBody().getAttributeValue("hold")); session.setHold(hold); } if (br.getBody().getAttribute("ver") != null) { String ver = br.getBody().getAttributeValue("ver"); session.setBoshVersion(ver); } if (br.getBody().getAttribute(NamespaceURIs.XML, "lang") != null) { String lang = br.getBody().getAttributeValue(NamespaceURIs.XML, "lang"); session.setXMLLang(lang); } if ("1".equals(br.getBody().getAttributeValue("ack"))) { session.setClientAcknowledgements(true); } session.insertRequest(br); sessions.put(session.getSessionId(), session); session.write0(getSessionCreationResponse(session)); } private Stanza getSessionCreationResponse(BoshBackedSessionContext session) { StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); body.addAttribute("wait", Integer.toString(session.getWait())); body.addAttribute("inactivity", Integer.toString(session.getInactivity())); body.addAttribute("polling", Integer.toString(session.getPolling())); body.addAttribute("requests", Integer.toString(session.getRequests())); body.addAttribute("hold", Integer.toString(session.getHold())); body.addAttribute("sid", session.getSessionId()); body.addAttribute("ver", session.getBoshVersion()); body.addAttribute("from", session.getServerJID().getFullQualifiedName()); body.addAttribute("secure", "true"); body.addAttribute("maxpause", Integer.toString(session.getMaxPause())); // adding the ack attribute here is needed because when responding to o request with the same RID (as is the case here) // the ack would not be included on BoshBackedSessionContext#write0, but this first ack is required. body.addAttribute("ack", Long.toString(session.getHighestReadRid())); Stanza features = new ServerResponses().getFeaturesForAuthentication(serverRuntimeContext.getServerFeatures() .getAuthenticationMethods(), session); body.addPreparedElement(features); return body.build(); } /** * Creates an empty BOSH response. * <p> * The empty BOSH response looks like <code><body xmlns='http://jabber.org/protocol/httpbind'/></code> * @return the empty BOSH response */ public Stanza getEmptyResponse() { StanzaBuilder stanzaBuilder = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); return stanzaBuilder.build(); } /** * Creates a BOSH response by wrapping a stanza in a <body/> element * @param stanza the XMPP stanza to wrap * @return the BOSH response */ public Stanza wrapStanza(Stanza stanza) { StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); body.addPreparedElement(stanza); return body.build(); } /** * Creates a BOSH response by merging 2 other BOSH responses, this is useful when sending more than one message as * a response to a HTTP request. * @param response1 the first BOSH response to merge * @param response2 the second BOSH response to merge * @return the merged BOSH response */ public Stanza mergeResponses(Stanza response1, Stanza response2) { if (response1 == null && response2 == null) { return null; } if (response1 == null) { return response2; } if (response2 == null) { return response1; } StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); for (XMLElement element : response1.getInnerElements()) { body.addPreparedElement(element); } for (XMLElement element : response2.getInnerElements()) { body.addPreparedElement(element); } return body.build(); } private Stanza getRestartResponse() { Stanza features = new ServerResponses().getFeaturesForSession(); return wrapStanza(features); } /** * Creates a session termination BOSH response * @return the termination BOSH body */ public Stanza getTerminateResponse() { StanzaBuilder stanzaBuilder = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); stanzaBuilder.addAttribute("type", "terminate"); return stanzaBuilder.build(); } /** * Adds a custom attribute to a BOSH body. * * @param stanza the BOSH body * @param attributeName the name of the attribute * @param attributeValue the value of the attribute * @return a new BOSH body identical with the one provided except it also has the newly added attribute */ public Stanza addAttribute(Stanza stanza, String attributeName, String attributeValue) { StanzaBuilder stanzaBuilder = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH); for (Attribute attr : stanza.getAttributes()) { stanzaBuilder.addAttribute(attr); } stanzaBuilder.addAttribute(attributeName, attributeValue); for (XMLElement element : stanza.getInnerElements()) { stanzaBuilder.addPreparedElement(element); } return stanzaBuilder.build(); } }