HttpSession.java 48.6 KB
Newer Older
Alex Wenckus's avatar
Alex Wenckus committed
1 2 3 4
/**
 * $Revision: $
 * $Date: $
 *
5
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
Alex Wenckus's avatar
Alex Wenckus committed
6
 *
7 8 9 10 11 12 13 14 15 16 17
 * Licensed 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.
Alex Wenckus's avatar
Alex Wenckus committed
18
 */
Matt Tucker's avatar
Matt Tucker committed
19

20
package org.jivesoftware.openfire.http;
Alex Wenckus's avatar
Alex Wenckus committed
21

22
import java.io.IOException;
23 24 25 26 27 28 29 30 31 32 33 34
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
35
import java.util.Locale;
36 37 38 39
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

40
import org.dom4j.*;
41
import org.dom4j.io.XMPPPacketReader;
42
import org.jivesoftware.openfire.*;
43
import org.jivesoftware.openfire.auth.UnauthorizedException;
44
import org.jivesoftware.openfire.multiplex.UnknownStanzaException;
45
import org.jivesoftware.openfire.net.MXParser;
46 47
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.VirtualConnection;
48
import org.jivesoftware.openfire.session.LocalClientSession;
49
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
50
import org.jivesoftware.util.JiveConstants;
51
import org.jivesoftware.util.JiveGlobals;
52
import org.jivesoftware.util.TaskEngine;
53 54
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
55 56 57 58
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
59
import org.xmpp.packet.Packet;
60
import org.xmpp.packet.Presence;
Alex Wenckus's avatar
Alex Wenckus committed
61

62 63 64 65
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;

Alex Wenckus's avatar
Alex Wenckus committed
66
/**
Daryl Herzmann's avatar
Daryl Herzmann committed
67
 * A session represents a series of interactions with an XMPP client sending packets using the HTTP
68 69 70
 * Binding protocol specified in <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>.
 * A session can have several client connections open simultaneously while awaiting packets bound
 * for the client from the server.
Alex Wenckus's avatar
Alex Wenckus committed
71 72 73
 *
 * @author Alexander Wenckus
 */
74
public class HttpSession extends LocalClientSession {
75 76 77
	
	private static final Logger Log = LoggerFactory.getLogger(HttpSession.class);

78 79 80 81 82 83 84 85 86 87 88 89
    private static XmlPullParserFactory factory = null;
    private static ThreadLocal<XMPPPacketReader> localParser = null;
    static {
        try {
            factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null);
            factory.setNamespaceAware(true);
        }
        catch (XmlPullParserException e) {
            Log.error("Error creating a parser factory", e);
        }
        // Create xmpp parser to keep in each thread
        localParser = new ThreadLocal<XMPPPacketReader>() {
90 91
            @Override
			protected XMPPPacketReader initialValue() {
92 93 94 95 96 97 98 99
                XMPPPacketReader parser = new XMPPPacketReader();
                factory.setNamespaceAware(true);
                parser.setXPPFactory(factory);
                return parser;
            }
        };
    }

Alex Wenckus's avatar
Alex Wenckus committed
100
    private int wait;
101
    private int hold = 0;
Alex Wenckus's avatar
Alex Wenckus committed
102
    private String language;
103 104 105
    private final List<HttpConnection> connectionQueue = Collections.synchronizedList(new LinkedList<HttpConnection>());
    private final List<Deliverable> pendingElements = Collections.synchronizedList(new ArrayList<Deliverable>());
    private final List<Delivered> sentElements = Collections.synchronizedList(new ArrayList<Delivered>());
Alex Wenckus's avatar
Alex Wenckus committed
106 107 108
    private boolean isSecure;
    private int maxPollingInterval;
    private long lastPoll = -1;
109
    private Set<SessionListener> listeners = new CopyOnWriteArraySet<>();
110
    private volatile boolean isClosed;
Alex Wenckus's avatar
Alex Wenckus committed
111
    private int inactivityTimeout;
112
    private int defaultInactivityTimeout;
113
    private long lastActivity;
114
    private long lastRequestID;
115
    private boolean lastResponseEmpty;
116
    private int maxRequests;
117
    private int maxPause;
118
    private PacketDeliverer backupDeliverer;
119 120
    private int majorVersion = -1;
    private int minorVersion = -1;
121
    private X509Certificate[] sslCertificates;
Alex Wenckus's avatar
Alex Wenckus committed
122

123
    private final Queue<Collection<Element>> packetsToSend = new LinkedList<>();
124
    // Semaphore which protects the packets to send, so, there can only be one consumer at a time.
125
    private SessionPacketRouter router;
126

127 128
    private static final Comparator<HttpConnection> connectionComparator
            = new Comparator<HttpConnection>() {
129
        @Override
130 131 132 133 134
        public int compare(HttpConnection o1, HttpConnection o2) {
            return (int) (o1.getRequestId() - o2.getRequestId());
        }
    };

135
    public HttpSession(PacketDeliverer backupDeliverer, String serverName, InetAddress address,
136 137
                       StreamID streamID, long rid, HttpConnection connection, Locale language) {
        super(serverName, new HttpVirtualConnection(address), streamID, language);
138
        this.isClosed = false;
139
        this.lastActivity = System.currentTimeMillis();
140
        this.lastRequestID = rid;
141
        this.backupDeliverer = backupDeliverer;
142
        this.sslCertificates = connection.getPeerCertificates();
Alex Wenckus's avatar
Alex Wenckus committed
143 144
    }

145 146 147 148 149
    /**
     * Returns the stream features which are available for this session.
     *
     * @return the stream features which are available for this session.
     */
Alex Wenckus's avatar
Alex Wenckus committed
150
    public Collection<Element> getAvailableStreamFeaturesElements() {
151
        List<Element> elements = new ArrayList<>();
Alex Wenckus's avatar
Alex Wenckus committed
152

153 154 155 156 157
        if (getAuthToken() == null) {
	        Element sasl = SASLAuthentication.getSASLMechanismsElement(this);
	        if (sasl != null) {
	            elements.add(sasl);
	        }
Alex Wenckus's avatar
Alex Wenckus committed
158 159
        }

160 161 162 163
        if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
            elements.add(DocumentHelper.createElement(new QName("register",
                    new Namespace("", "http://jabber.org/features/iq-register"))));
        }
Alex Wenckus's avatar
Alex Wenckus committed
164 165 166 167 168 169
        Element bind = DocumentHelper.createElement(new QName("bind",
                new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind")));
        elements.add(bind);

        Element session = DocumentHelper.createElement(new QName("session",
                new Namespace("", "urn:ietf:params:xml:ns:xmpp-session")));
170
        session.addElement("optional");
Alex Wenckus's avatar
Alex Wenckus committed
171 172 173 174
        elements.add(session);
        return elements;
    }

175 176
    @Override
	public String getAvailableStreamFeatures() {
Alex Wenckus's avatar
Alex Wenckus committed
177
        StringBuilder sb = new StringBuilder(200);
178
        for (Element element : getAvailableStreamFeaturesElements()) {
Alex Wenckus's avatar
Alex Wenckus committed
179 180 181 182 183
            sb.append(element.asXML());
        }
        return sb.toString();
    }

184 185 186 187
    /**
     * Closes the session. After a session has been closed it will no longer accept new connections
     * on the session ID.
     */
188 189
    @Override
	public void close() {
190 191 192 193
        if (isClosed) {
            return;
        }
        conn.close();
Alex Wenckus's avatar
Alex Wenckus committed
194 195
    }

196
    /**
197
     * Returns true if this session has been closed and no longer actively accepting connections.
198
     *
199
     * @return true if this session has been closed and no longer actively accepting connections.
200
     */
201
    @Override
202
	public boolean isClosed() {
Alex Wenckus's avatar
Alex Wenckus committed
203 204 205 206
        return isClosed;
    }

    /**
207 208 209 210
     * Specifies the longest time (in seconds) that the connection manager is allowed to wait before
     * responding to any request during the session. This enables the client to prevent its TCP
     * connection from expiring due to inactivity, as well as to limit the delay before it discovers
     * any network failure.
Alex Wenckus's avatar
Alex Wenckus committed
211 212 213 214 215 216 217 218
     *
     * @param wait the longest time it is permissible to wait for a response.
     */
    public void setWait(int wait) {
        this.wait = wait;
    }

    /**
219 220 221 222
     * Specifies the longest time (in seconds) that the connection manager is allowed to wait before
     * responding to any request during the session. This enables the client to prevent its TCP
     * connection from expiring due to inactivity, as well as to limit the delay before it discovers
     * any network failure.
Alex Wenckus's avatar
Alex Wenckus committed
223 224 225 226 227 228 229
     *
     * @return the longest time it is permissible to wait for a response.
     */
    public int getWait() {
        return wait;
    }

230 231 232 233 234 235 236 237 238 239 240 241
    /**
     * Specifies the maximum number of requests the connection manager is allowed to keep waiting at
     * any one time during the session. (For example, if a constrained client is unable to keep open
     * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
     * a value of "1".)
     *
     * @param hold the maximum number of simultaneous waiting requests.
     */
    public void setHold(int hold) {
        this.hold = hold;
    }

Alex Wenckus's avatar
Alex Wenckus committed
242
    /**
243 244 245 246
     * Specifies the maximum number of requests the connection manager is allowed to keep waiting at
     * any one time during the session. (For example, if a constrained client is unable to keep open
     * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
     * a value of "1".)
Alex Wenckus's avatar
Alex Wenckus committed
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
     *
     * @return the maximum number of simultaneous waiting requests
     */
    public int getHold() {
        return hold;
    }

    /**
     * Sets the max interval within which a client can send polling requests. If more than one
     * request occurs in the interval the session will be terminated.
     *
     * @param maxPollingInterval time in seconds a client needs to wait before sending polls to the
     * server, a negative <i>int</i> indicates that there is no limit.
     */
    public void setMaxPollingInterval(int maxPollingInterval) {
        this.maxPollingInterval = maxPollingInterval;
    }

265 266 267 268 269
    /**
     * Returns the max interval within which a client can send polling requests. If more than one
     * request occurs in the interval the session will be terminated.
     *
     * @return the max interval within which a client can send polling requests. If more than one
270
     *         request occurs in the interval the session will be terminated.
271 272 273 274 275 276
     */
    public int getMaxPollingInterval() {
        return this.maxPollingInterval;
    }

    /**
277
     * The max number of requests it is permissible for this session to have open at any one time.
278
     *
279
     * @param maxRequests The max number of requests it is permissible for this session to have open
280
     * at any one time.
281 282 283 284 285 286
     */
    public void setMaxRequests(int maxRequests) {
        this.maxRequests = maxRequests;
    }

    /**
287
     * Returns the max number of requests it is permissible for this session to have open at any one
288 289
     * time.
     *
290
     * @return the max number of requests it is permissible for this session to have open at any one
291
     *         time.
292 293 294 295 296
     */
    public int getMaxRequests() {
        return this.maxRequests;
    }

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    /**
     * Sets the maximum length of a temporary session pause (in seconds) that the client MAY
     * request.
     *
     * @param maxPause the maximum length of a temporary session pause (in seconds) that the client
     * MAY request.
     */
    public void setMaxPause(int maxPause) {
        this.maxPause = maxPause;
    }

    /**
     * Returns the maximum length of a temporary session pause (in seconds) that the client MAY
     * request.
     *
     * @return the maximum length of a temporary session pause (in seconds) that the client MAY
     *         request.
     */
    public int getMaxPause() {
        return this.maxPause;
    }

Alex Wenckus's avatar
Alex Wenckus committed
319
    /**
320 321
     * Returns true if all connections on this session should be secured, and false if they should
     * not.
Alex Wenckus's avatar
Alex Wenckus committed
322
     *
323 324
     * @return true if all connections on this session should be secured, and false if they should
     *         not.
Alex Wenckus's avatar
Alex Wenckus committed
325
     */
326 327
    @Override
	public boolean isSecure() {
328
        return isSecure;
Alex Wenckus's avatar
Alex Wenckus committed
329 330
    }

331
    /**
332 333 334 335 336
     * Returns true if this session is a polling session. Some clients may be restricted to open
     * only one connection to the server. In this case the client SHOULD inform the server by
     * setting the values of the 'wait' and/or 'hold' attributes in its session creation request
     * to "0", and then "poll" the server at regular intervals throughout the session for stanzas
     * it may have received from the server.
337 338 339 340 341 342 343
     *
     * @return true if this session is a polling session.
     */
    public boolean isPollingSession() {
        return (this.wait == 0 || this.hold == 0);
    }

Alex Wenckus's avatar
Alex Wenckus committed
344
    /**
345
     * Adds a {@link org.jivesoftware.openfire.http.SessionListener} to this session. The listener
346
     * will be notified of changes to the session.
Alex Wenckus's avatar
Alex Wenckus committed
347
     *
348
     * @param listener the listener which is being added to the session.
Alex Wenckus's avatar
Alex Wenckus committed
349 350 351 352 353
     */
    public void addSessionCloseListener(SessionListener listener) {
        listeners.add(listener);
    }

354
    /**
355
     * Removes a {@link org.jivesoftware.openfire.http.SessionListener} from this session. The
356 357 358 359
     * listener will no longer be updated when an event occurs on the session.
     *
     * @param listener the session listener that is to be removed.
     */
Alex Wenckus's avatar
Alex Wenckus committed
360 361 362 363
    public void removeSessionCloseListener(SessionListener listener) {
        listeners.remove(listener);
    }

364 365 366 367 368 369 370 371 372 373 374 375
    /**
     * Sets the default inactivity timeout of this session. A session's inactivity timeout can
     * be temporarily changed using session pause requests.
     *
     * @see #pause(int)
     *
     * @param defaultInactivityTimeout the default inactivity timeout of this session.
     */
    public void setDefaultInactivityTimeout(int defaultInactivityTimeout) {
        this.defaultInactivityTimeout = defaultInactivityTimeout;
    }

376 377 378 379 380 381 382
    /**
     * Sets the time, in seconds, after which this session will be considered inactive and be be
     * terminated.
     *
     * @param inactivityTimeout the time, in seconds, after which this session will be considered
     * inactive and be terminated.
     */
Alex Wenckus's avatar
Alex Wenckus committed
383 384 385 386
    public void setInactivityTimeout(int inactivityTimeout) {
        this.inactivityTimeout = inactivityTimeout;
    }

387 388 389 390 391 392 393 394 395 396
    /**
     * Resets the inactivity timeout of this session to default. A session's inactivity timeout can
     * be temporarily changed using session pause requests.
     *
     * @see #pause(int)
     */
    public void resetInactivityTimeout() {
        this.inactivityTimeout = this.defaultInactivityTimeout;
    }

397 398 399 400 401 402 403
    /**
     * Returns the time, in seconds, after which this session will be considered inactive and
     * terminated.
     *
     * @return the time, in seconds, after which this session will be considered inactive and
     *         terminated.
     */
Alex Wenckus's avatar
Alex Wenckus committed
404 405 406 407
    public int getInactivityTimeout() {
        return inactivityTimeout;
    }

408 409 410 411 412 413 414 415 416 417 418 419
    /**
     * Pauses the session for the given amount of time. If a client encounters an exceptional
     * temporary situation during which it will be unable to send requests to the connection
     * manager for a period of time greater than the maximum inactivity period, then the client MAY
     * request a temporary increase to the maximum inactivity period by including a 'pause'
     * attribute in a request.
     *
     * @param duration the time, in seconds, after which this session will be considered inactive
     *        and terminated.
     */
    public void pause(int duration) {
    	// Respond immediately to all pending requests
420 421 422 423 424 425 426 427
    	synchronized (connectionQueue) {
	        for (HttpConnection toClose : connectionQueue) {
	            if (!toClose.isClosed()) {
	                toClose.close();
	                lastRequestID = toClose.getRequestId();
	            }
	        }
    	}
428 429 430
    	setInactivityTimeout(duration);
    }

431 432
    /**
     * Returns the time in milliseconds since the epoch that this session was last active. Activity
433 434
     * is a request was either made or responded to. If the session is currently active, meaning
     * there are connections awaiting a response, the current time is returned.
435 436 437
     *
     * @return the time in milliseconds since the epoch that this session was last active.
     */
438 439 440 441 442
    public long getLastActivity() {
        if (!connectionQueue.isEmpty()) {
        	synchronized (connectionQueue) {
	            for (HttpConnection connection : connectionQueue) {
	                // The session is currently active, set the last activity to the current time.
443
	                if (!(connection.isClosed())) {
444 445 446 447 448 449 450
	                    lastActivity = System.currentTimeMillis();
	                    break;
	                }
	            }
        	}
        }
        return lastActivity;
451 452
    }

453
    /**
454 455 456 457 458 459 460 461 462 463 464 465
     * Returns the highest 'rid' attribute the server has received where it has also received
     * all requests with lower 'rid' values. When responding to a request that it has been
     * holding, if the server finds it has already received another request with a higher 'rid'
     * attribute (typically while it was holding the first request), then it MAY acknowledge the
     * reception to the client.
     *
     * @return the highest 'rid' attribute the server has received where it has also received
     * all requests with lower 'rid' values.
     */
    public long getLastAcknowledged() {
    	long ack = lastRequestID;
    	Collections.sort(connectionQueue, connectionComparator);
466 467 468 469 470 471 472
    	synchronized (connectionQueue) {
	        for (HttpConnection connection : connectionQueue) {
	            if (connection.getRequestId() == ack + 1) {
	            	ack++;
	            }
	        }
    	}
473 474 475 476 477 478
        return ack;
    }

    /**
     * Sets the major version of BOSH which the client implements. Currently, the only versions
     * supported by Openfire are 1.5 and 1.6.
479
     *
480
     * @param majorVersion the major version of BOSH which the client implements.
481
     */
482 483
    public void setMajorVersion(int majorVersion) {
        if(majorVersion != 1) {
484 485
            return;
        }
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
        this.majorVersion = majorVersion;
    }

    /**
     * Returns the major version of BOSH which this session utilizes. The version refers to the
     * version of the XEP which the connecting client implements. If the client did not specify
     * a version 1 is returned as 1.5 is the last version of the <a
     * href="http://www.xmpp.org/extensions/xep-0124.html">XEP</a> that the client was not
     * required to pass along its version information when creating a session.
     *
     * @return the major version of the BOSH XEP which the client is utilizing.
     */
    public int getMajorVersion() {
        if (this.majorVersion != -1) {
            return this.majorVersion;
        }
        else {
            return 1;
504 505 506 507
        }
    }

    /**
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
     * Sets the minor version of BOSH which the client implements. Currently, the only versions
     * supported by Openfire are 1.5 and 1.6. Any versions less than or equal to 5 will be
     * interpreted as 5 and any values greater than or equal to 6 will be interpreted as 6.
     *
     * @param minorVersion the minor version of BOSH which the client implements.
     */
    public void setMinorVersion(int minorVersion) {
    	if(minorVersion <= 5) {
        	this.minorVersion = 5;
        }
    	else if(minorVersion >= 6) {
        	this.minorVersion = 6;
        }
    }

    /**
     * Returns the major version of BOSH which this session utilizes. The version refers to the
525
     * version of the XEP which the connecting client implements. If the client did not specify
526
     * a version 5 is returned as 1.5 is the last version of the <a
527 528 529
     * href="http://www.xmpp.org/extensions/xep-0124.html">XEP</a> that the client was not
     * required to pass along its version information when creating a session.
     *
530
     * @return the minor version of the BOSH XEP which the client is utilizing.
531
     */
532 533 534
    public int getMinorVersion() {
        if (this.minorVersion != -1) {
            return this.minorVersion;
535 536
        }
        else {
537
            return 5;
538 539 540
        }
    }

541 542 543 544 545 546 547 548 549 550
    /**
     * lastResponseEmpty true if last response of this session is an empty body element. This
     * is used in overactivity checking.
     *
     * @param lastResponseEmpty true if last response of this session is an empty body element.
     */
	public void setLastResponseEmpty(boolean lastResponseEmpty) {
		this.lastResponseEmpty = lastResponseEmpty;
	}

551
    /**
552 553 554
     * Sets whether the initial request on the session was secure.
     *
     * @param isSecure true if the initial request was secure and false if it wasn't.
555
     */
556 557
    protected void setSecure(boolean isSecure) {
        this.isSecure = isSecure;
558
    }
559

560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
    /**
     * Forwards a client request, which is related to a session, to the server. A connection is
     * created and queued up in the provided session. When a connection reaches the top of a queue
     * any pending packets bound for the client will be forwarded to the client through the
     * connection.
     *
     * @param rid the unique, sequential, requestID sent from the client.
     * @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
     * was not.
     * @param rootNode the XML body of the request.
     * @param context the context of the asynchronous servlet call leading up to this method call.
     *
     * @throws org.jivesoftware.openfire.http.HttpBindException for several reasons: if the encoding inside of an auth packet is
     * not recognized by the server, or if the packet type is not recognized.
     * @throws org.jivesoftware.openfire.http.HttpConnectionClosedException if the session is no longer available.
     */
    public void forwardRequest(long rid, boolean isSecure, Element rootNode, AsyncContext context)
            throws HttpBindException, HttpConnectionClosedException, IOException
    {
        List<Element> elements = rootNode.elements();
        boolean isPoll = (elements.size() == 0);
        if ("terminate".equals(rootNode.attributeValue("type")))
            isPoll = false;
        else if ("true".equals(rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp")))))
            isPoll = false;
        else if (rootNode.attributeValue("pause") != null)
            isPoll = false;
587
        HttpConnection connection = this.createConnection(rid, isSecure, isPoll, context);
588 589
        if (elements.size() > 0) {
            // creates the runnable to forward the packets
590
            packetsToSend.add(elements);
591
            new HttpPacketSender(this).init();
592
        }
593 594 595 596 597 598

        final String type = rootNode.attributeValue("type");
        String restartStream = rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp")));
        int pauseDuration = HttpBindServlet.getIntAttribute(rootNode.attributeValue("pause"), -1);

        if ("terminate".equals(type)) {
599
            connection.deliverBody(createEmptyBody(true), true);
600
            close();
601 602
            lastRequestID = connection.getRequestId();
        }
603
        else if ("true".equals(restartStream) && rootNode.elements().size() == 0) {
604
            connection.deliverBody(createSessionRestartResponse(), true);
605 606 607 608
            lastRequestID = connection.getRequestId();
        }
        else if (pauseDuration > 0 && pauseDuration <= getMaxPause()) {
            pause(pauseDuration);
609
            connection.deliverBody(createEmptyBody(false), true);
610
            lastRequestID = connection.getRequestId();
611
            setLastResponseEmpty(true);
612
        }
613 614 615
        else {
            resetInactivityTimeout();
        }
616 617
    }

618
    /**
619 620
     * This methods sends any pending packets in the session. If no packets are
     * pending, this method simply returns. The method is internally synchronized
621 622
     * to avoid simultaneous sending operations on this Session. If two
     * threads try to run this method simultaneously, the first one will trigger
623 624
     * the pending packets to be sent, while the second one will simply return
     * (as there are no packets left to send).
625
     */
626 627 628
    protected void sendPendingPackets() {
        // access blocked only on send to prevent deadlocks
        synchronized (packetsToSend) {
629
            if (packetsToSend.isEmpty()) {
630
                return;
631 632
            }

633 634 635 636 637 638 639 640 641 642 643 644 645
            if (router == null) {
                router = new SessionPacketRouter(this);
            }

            for (Element packet : packetsToSend.remove()) {
                try {
                    router.route(packet);
                }
                catch (UnknownStanzaException e) {
                    Log.error("Client provided unknown packet type", e);
                }
            }
        }
646
    }
647

648 649
    /**
     * Return the X509Certificates associated with this session.
650
     *
651 652
     * @return the X509Certificate associated with this session.
     */
653
    @Override
654 655 656
    public X509Certificate[] getPeerCertificates() {
        return sslCertificates;
    }
657

658 659 660 661 662 663 664
    /**
     * Creates a new connection on this session. If a response is currently available for this
     * session the connection is responded to immediately, otherwise it is queued awaiting a
     * response.
     *
     * @param rid the request id related to the connection.
     * @param isSecure true if the connection was secured using HTTPS.
665
     * @return the created {@link org.jivesoftware.openfire.http.HttpConnection} which represents
666 667 668 669
     *         the connection.
     *
     * @throws HttpConnectionClosedException if the connection was closed before a response could be
     * delivered.
670 671 672
     * @throws HttpBindException if the connection has violated a facet of the HTTP binding
     * protocol.
     */
673
    synchronized HttpConnection createConnection(long rid, boolean isSecure, boolean isPoll, AsyncContext context)
674
            throws HttpConnectionClosedException, HttpBindException, IOException
675
    {
676
        final HttpConnection connection = new HttpConnection(rid, isSecure, sslCertificates, context);
677
        connection.setSession(this);
678 679 680 681
        context.setTimeout(getWait() * JiveConstants.SECOND);
        context.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent asyncEvent) throws IOException {
682
                Log.debug("complete event " + asyncEvent);
683 684 685 686 687 688
                connectionQueue.remove(connection);
                fireConnectionClosed(connection);
            }

            @Override
            public void onTimeout(AsyncEvent asyncEvent) throws IOException {
689
                Log.debug("timeout event " + asyncEvent);
690
                try {
691 692
                    // If onTimeout does not result in a complete(), the container falls back to default behavior.
                    // This is why this body is to be delivered in a non-async fashion.
693
                    connection.deliverBody(createEmptyBody(false), false);
694 695 696 697 698 699 700 701 702 703
                    setLastResponseEmpty(true);

                    // This connection timed out we need to increment the request count
                    if (connection.getRequestId() != lastRequestID + 1) {
                        throw new IOException("Unexpected RID error.");
                    }
                    lastRequestID = connection.getRequestId();
                } catch (HttpConnectionClosedException e) {
                    Log.warn("Unexpected exception while processing connection timeout.", e);
                }
704 705

                // Note that 'onComplete' will be invoked.
706 707 708 709
            }

            @Override
            public void onError(AsyncEvent asyncEvent) throws IOException {
710
                Log.debug("error event " + asyncEvent);
711
                Log.warn("Unhandled AsyncListener error: " + asyncEvent.getThrowable());
712 713
                connectionQueue.remove(connection);
                fireConnectionClosed(connection);
714 715 716 717 718 719
            }

            @Override
            public void onStartAsync(AsyncEvent asyncEvent) throws IOException {}
        });

720
        if (rid <= lastRequestID) {
721
            Delivered deliverable = retrieveDeliverable(rid);
722 723
            if (deliverable == null) {
                Log.warn("Deliverable unavailable for " + rid);
724 725
                throw new HttpBindException("Unexpected RID error.",
                        BoshBindingError.itemNotFound);
726
            }
727
            connection.deliverBody(createDeliverable(deliverable.deliverables), true);
728
            addConnection(connection, isPoll);
729 730
            return connection;
        }
731 732
        else if (rid > (lastRequestID + maxRequests)) {
            Log.warn("Request " + rid + " > " + (lastRequestID + maxRequests) + ", ending session.");
733 734
                throw new HttpBindException("Unexpected RID error.",
                        BoshBindingError.itemNotFound);
735 736
        }

737
        addConnection(connection, isPoll);
738 739 740
        return connection;
    }

741
    private Delivered retrieveDeliverable(long rid) {
742 743 744 745 746 747 748 749 750 751
    	Delivered result = null;
    	synchronized (sentElements) {
	        for (Delivered delivered : sentElements) {
	            if (delivered.getRequestID() == rid) {
	                result = delivered;
	                break;
	            }
	        }
    	}
        return result;
752 753 754
    }

    private void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException,
755
            HttpConnectionClosedException, IOException {
756 757 758
        if (connection == null) {
            throw new IllegalArgumentException("Connection cannot be null.");
        }
759
        
760 761
        if (isSecure && !connection.isSecure()) {
            throw new HttpBindException("Session was started from secure connection, all " +
762
                    "connections on this session must be secured.", BoshBindingError.badRequest);
763 764
        }

765 766 767 768 769 770 771 772
        final long rid = connection.getRequestId();

        /*
         * Search through the connection queue to see if this rid already exists on it. If it does then we
         * will close and deliver the existing connection (if appropriate), and close and deliver the same
         * deliverable on the new connection. This is under the assumption that a connection has been dropped,
         * and re-requested before jetty has realised.
         */
773 774 775
        synchronized (connectionQueue) {
			for (HttpConnection queuedConnection : connectionQueue) {
				if (queuedConnection.getRequestId() == rid) {
776
					if(Log.isDebugEnabled()) {
777
						Log.debug("Found previous connection in queue with rid " + rid);
778
					}
779 780 781 782 783 784 785 786 787 788 789
					if(queuedConnection.isClosed()) {
						if(Log.isDebugEnabled()) {
							Log.debug("It's closed - copying deliverables");
						}
						
			            Delivered deliverable = retrieveDeliverable(rid);
			            if (deliverable == null) {
			                Log.warn("Deliverable unavailable for " + rid);
			                throw new HttpBindException("Unexpected RID error.",
			                        BoshBindingError.itemNotFound);
			            }
790
			            connection.deliverBody(createDeliverable(deliverable.deliverables), true);
791 792 793 794 795 796 797 798 799 800
					} else {
						if(Log.isDebugEnabled()) {
							Log.debug("It's still open - calling close()");
						}
						deliver(queuedConnection, Collections.singleton(new Deliverable("")));
						connection.close();
						
						if(rid == (lastRequestID + 1)) {
							lastRequestID = rid;
						}
801
					}
802
					break;
803 804
				}
			}
805
        }
806 807 808

        checkOveractivity(isPoll);

809
        sslCertificates = connection.getPeerCertificates();
810

811 812
        // We aren't supposed to hold connections open or we already have some packets waiting
        // to be sent to the client.
813
        if (isPollingSession() || (pendingElements.size() > 0 && connection.getRequestId() == lastRequestID + 1)) {
814 815 816 817 818 819
            fireConnectionOpened(connection);
            synchronized(pendingElements) {
                deliver(connection, pendingElements);
                lastRequestID = connection.getRequestId();
                pendingElements.clear();
            }
820 821 822 823
        }
        else {
            // With this connection we need to check if we will have too many connections open,
            // closing any extras.
824

825 826
            connectionQueue.add(connection);
            Collections.sort(connectionQueue, connectionComparator);
827

828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
            synchronized (connectionQueue) {
	            int connectionsToClose;
	            if(connectionQueue.get(connectionQueue.size() - 1) != connection) {
	            	// Current connection does not have the greatest rid. That means
	            	// requests were received out of order, respond to all.
	            	connectionsToClose = connectionQueue.size();
	            }
	            else {
	                // Everything's fine, number of current connections open tells us
	            	// how many that we need to close.
	            	connectionsToClose = getOpenConnectionCount() - hold;
	            }
	            int closed = 0;
	            for (int i = 0; i < connectionQueue.size() && closed < connectionsToClose; i++) {
	                HttpConnection toClose = connectionQueue.get(i);
	                if (!toClose.isClosed() && toClose.getRequestId() == lastRequestID + 1) {
	                    if(toClose == connection) {
	                    	// Current connection has no continuation yet, just deliver.
	                    	deliver("");
	                    }
	                    else {
	                        toClose.close();
	                    }
	                    lastRequestID = toClose.getRequestId();
	                    closed++;
	                }
	            }
855 856 857 858
            }
        }
    }

859 860
    private int getOpenConnectionCount() {
        int count = 0;
861
        // NOTE: synchronized by caller
862 863
        for (HttpConnection connection : connectionQueue) {
            if (!connection.isClosed()) {
864 865 866 867 868 869
                count++;
            }
        }
        return count;
    }

870
    private void deliver(HttpConnection connection, Collection<Deliverable> deliverable)
871
            throws HttpConnectionClosedException, IOException {
872
        connection.deliverBody(createDeliverable(deliverable), true);
873

874
        Delivered delivered = new Delivered(deliverable);
875
        delivered.setRequestID(connection.getRequestId());
Tom Evans's avatar
Tom Evans committed
876
        while (sentElements.size() > maxRequests) {
877 878 879 880 881 882 883 884 885 886 887 888 889
            sentElements.remove(0);
        }

        sentElements.add(delivered);
    }

    private void fireConnectionOpened(HttpConnection connection) {
        lastActivity = System.currentTimeMillis();
        for (SessionListener listener : listeners) {
            listener.connectionOpened(this, connection);
        }
    }

890 891 892 893 894
    /**
     * Check that the client SHOULD NOT make more simultaneous requests than specified
     * by the 'requests' attribute in the connection manager's Session Creation Response.
     * However the client MAY make one additional request if it is to pause or terminate a session.
     *
895
     * @see <a href="http://www.xmpp.org/extensions/xep-0124.html#overactive">overactive</a>
896 897 898 899 900 901 902 903 904
     * @param isPoll true if the session is using polling.
     * @throws HttpBindException if the connection has violated a facet of the HTTP binding
     *         protocol.
     */
    private void checkOveractivity(boolean isPoll) throws HttpBindException {
    	int pendingConnections = 0;
    	boolean overactivity = false;
    	String errorMessage = "Overactivity detected";

905 906 907 908 909 910
    	synchronized (connectionQueue) {
    		for (HttpConnection conn : connectionQueue) {
    			if (!conn.isClosed()) {
    				pendingConnections++;
    			}
    		}
911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
        }

        if(pendingConnections >= maxRequests) {
        	overactivity = true;
        	errorMessage += ", too many simultaneous requests.";
        }
        else if(isPoll) {
	    	long time = System.currentTimeMillis();
	        if (time - lastPoll < maxPollingInterval * JiveConstants.SECOND) {
	        	if(isPollingSession()) {
	        		overactivity = lastResponseEmpty;
	        	}
	        	else {
	        		overactivity = (pendingConnections >= maxRequests - 1);
	        	}
	        }
	        errorMessage += ", minimum polling interval is "
	        	+ maxPollingInterval + ", current interval " + ((time - lastPoll) / 1000);
929
	        lastPoll = time;
930 931 932 933 934 935 936
        }
        setLastResponseEmpty(false);

        if(overactivity) {
        	Log.debug(errorMessage);
            if (!JiveGlobals.getBooleanProperty("xmpp.httpbind.client.requests.ignoreOveractivity", false)) {
                throw new HttpBindException(errorMessage, BoshBindingError.policyViolation);
937
            }
938 939 940
        }
    }

941
    private void deliver(String text) {
942 943 944 945
        if (text == null) {
            // Do nothing if someone asked to send nothing :)
            return;
        }
946 947 948
        deliver(new Deliverable(text));
    }

949
    @Override
950
    public void deliver(Packet stanza) {
951
        deliver(new Deliverable(Arrays.asList(stanza)));
952 953 954
    }

    private void deliver(Deliverable stanza) {
955
        Collection<Deliverable> deliverable = Arrays.asList(stanza);
956
        boolean delivered = false;
957
        int pendingConnections = 0;
958 959
        synchronized (connectionQueue) {
	        for (HttpConnection connection : connectionQueue) {
960 961 962 963
                if (connection.isClosed()) {
                    continue;
                }
                pendingConnections++;
964 965 966 967 968 969 970 971 972
	            try {
	                if (connection.getRequestId() == lastRequestID + 1) {
	                    lastRequestID = connection.getRequestId();
	                    deliver(connection, deliverable);
	                    delivered = true;
	                    break;
	                }
	            }
	            catch (HttpConnectionClosedException e) {
973 974 975
	                /* Connection was closed, try the next one. Indicates a (concurrency?) bug. */
	                Log.warn("Iterating over a connection that was closed. Openfire will recover from this problem, but it should not occur in the first place.");
                } catch (IOException e) {
976
                    Log.warn("An unexpected exception occurred while iterating over connections. Openfire will attempt to recover by ignoring this connection.", e);
977 978
                }
            }
979 980 981
        }

        if (!delivered) {
982 983 984
            if (pendingConnections > 0) {
                Log.warn("Unable to deliver a stanza (it is being queued instead), although there are available connections! RID / Connection processing is out of sync!");
            }
985 986 987 988 989 990 991 992 993 994 995 996 997
            pendingElements.add(stanza);
        }
    }

    private void fireConnectionClosed(HttpConnection connection) {
        lastActivity = System.currentTimeMillis();
        for (SessionListener listener : listeners) {
            listener.connectionClosed(this, connection);
        }
    }

    private String createDeliverable(Collection<Deliverable> elements) {
        StringBuilder builder = new StringBuilder();
998 999
        builder.append("<body xmlns='http://jabber.org/protocol/httpbind' ack='")
        		.append(getLastAcknowledged()).append("'>");
1000 1001

        setLastResponseEmpty(elements.size() == 0);
1002 1003 1004 1005
        synchronized (elements) {
	        for (Deliverable child : elements) {
	            builder.append(child.getDeliverable());
	        }
1006 1007 1008 1009 1010
        }
        builder.append("</body>");
        return builder.toString();
    }

1011
    private void closeSession() {
1012 1013
        if (isClosed) { return; }
        isClosed = true;
1014

1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
        try {
	        // close connection(s) and deliver pending elements (if any)
	        synchronized (connectionQueue) {
		        for (HttpConnection toClose : connectionQueue) {
		            try {
		            	if (!toClose.isClosed()) {
		            		if (!pendingElements.isEmpty() && toClose.getRequestId() == lastRequestID + 1) {
		            			synchronized(pendingElements) {
			            			deliver(toClose, pendingElements);
					                lastRequestID = toClose.getRequestId();
					                pendingElements.clear();
		            			}
	            			} else {
1028
	            				toClose.deliverBody(null, true);
1029
	            			}
1030 1031 1032
		            	}
		            } catch (HttpConnectionClosedException e) {
		            	/* ignore ... already closed */
1033
                    } catch (IOException e) {
1034 1035
                        // Likely caused by closing a stale session / connection.
                        Log.debug("An unexpected exception occurred while closing a session.", e);
1036 1037
		            }
		        }
1038
	        }
1039 1040 1041 1042 1043 1044
	
	    	synchronized (pendingElements) {
		        for (Deliverable deliverable : pendingElements) {
		            failDelivery(deliverable.getPackets());
		        }
		        pendingElements.clear();
1045
	        }
1046 1047 1048 1049 1050
        } finally { // ensure the session is removed from the session map
	        for (SessionListener listener : listeners) {
	            listener.sessionClosed(this);
	        }
	        this.listeners.clear();
1051 1052 1053
        }
    }

1054
    private void failDelivery(final Collection<Packet> packets) {
1055 1056 1057 1058
        if (packets == null) {
            // Do nothing if someone asked to deliver nothing :)
            return;
        }
1059 1060
        // use a separate thread to schedule backup delivery
   		TaskEngine.getInstance().submit(new Runnable() {
1061
			@Override
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072
			public void run() {
		        for (Packet packet : packets) {
    	            try {
        				backupDeliverer.deliver(packet);
    	            }
    	            catch (UnauthorizedException e) {
    	                Log.error("Unable to deliver message to backup deliverer", e);
    	            }
		        }
			}
   		});
1073 1074
    }

1075
    protected String createEmptyBody(boolean terminate)
1076
    {
1077
        final Element body = DocumentHelper.createElement( QName.get( "body", "http://jabber.org/protocol/httpbind" ) );
1078
        if (terminate) { body.addAttribute("type", "terminate"); }
1079
        body.addAttribute("ack", String.valueOf(getLastAcknowledged()));
1080 1081 1082 1083 1084
        return body.asXML();
    }

    private String createSessionRestartResponse()
    {
1085
        final Element response = DocumentHelper.createElement( QName.get( "body", "http://jabber.org/protocol/httpbind" ) );
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
        response.addNamespace("stream", "http://etherx.jabber.org/streams");

        final Element features = response.addElement("stream:features");
        for (Element feature : getAvailableStreamFeaturesElements()) {
            features.add(feature);
        }

        return response.asXML();
    }

1096 1097 1098
    /**
     * A virtual server connection relates to a http session which its self can relate to many http
     * connections.
Alex Wenckus's avatar
Alex Wenckus committed
1099 1100 1101 1102
     */
    public static class HttpVirtualConnection extends VirtualConnection {

        private InetAddress address;
1103
        private ConnectionConfiguration configuration;
Alex Wenckus's avatar
Alex Wenckus committed
1104 1105 1106 1107 1108

        public HttpVirtualConnection(InetAddress address) {
            this.address = address;
        }

1109 1110
        @Override
		public void closeVirtualConnection() {
1111
            ((HttpSession) session).closeSession();
Alex Wenckus's avatar
Alex Wenckus committed
1112 1113
        }

1114
        @Override
1115 1116 1117 1118
        public byte[] getAddress() throws UnknownHostException {
            return address.getAddress();
        }

1119
        @Override
1120 1121 1122 1123
        public String getHostAddress() throws UnknownHostException {
            return address.getHostAddress();
        }

1124
        @Override
1125 1126
        public String getHostName() throws UnknownHostException {
            return address.getHostName();
Alex Wenckus's avatar
Alex Wenckus committed
1127 1128
        }

1129
        @Override
Alex Wenckus's avatar
Alex Wenckus committed
1130
        public void systemShutdown() {
1131
            close();
Alex Wenckus's avatar
Alex Wenckus committed
1132 1133
        }

1134
        @Override
Alex Wenckus's avatar
Alex Wenckus committed
1135
        public void deliver(Packet packet) throws UnauthorizedException {
1136
            ((HttpSession) session).deliver(packet);
Alex Wenckus's avatar
Alex Wenckus committed
1137 1138
        }

1139
        @Override
Alex Wenckus's avatar
Alex Wenckus committed
1140
        public void deliverRawText(String text) {
1141
            ((HttpSession) session).deliver(text);
Alex Wenckus's avatar
Alex Wenckus committed
1142
        }
1143

1144 1145 1146 1147 1148
        @Override
        public ConnectionConfiguration getConfiguration() {
            return session.getConnection().getConfiguration();
        }

1149 1150
        @Override
		public Certificate[] getPeerCertificates() {
1151 1152
            return ((HttpSession) session).getPeerCertificates();
        }
Alex Wenckus's avatar
Alex Wenckus committed
1153 1154
    }

1155
    static class Deliverable {
Alex Wenckus's avatar
Alex Wenckus committed
1156
        private final String text;
1157
        private final Collection<String> packets;
Alex Wenckus's avatar
Alex Wenckus committed
1158 1159 1160

        public Deliverable(String text) {
            this.text = text;
1161
            this.packets = null;
Alex Wenckus's avatar
Alex Wenckus committed
1162 1163
        }

1164
        public Deliverable(Collection<Packet> elements) {
Alex Wenckus's avatar
Alex Wenckus committed
1165
            this.text = null;
1166
            this.packets = new ArrayList<>();
1167
            for (Packet packet : elements) {
1168 1169
                // Append packet namespace according XEP-0206 if needed
            	if (Namespace.NO_NAMESPACE.equals(packet.getElement().getNamespace())) {
1170 1171
            		// use string-based operation here to avoid cascading xmlns wonkery
            		StringBuilder packetXml = new StringBuilder(packet.toXML());
1172 1173 1174 1175
                    final int noslash = packetXml.indexOf( ">" );
                    final int slash = packetXml.indexOf( "/>" );
                    final int insertAt = ( noslash - 1 == slash ? slash : noslash );
            		packetXml.insert( insertAt, " xmlns=\"jabber:client\"");
1176 1177 1178
            		this.packets.add(packetXml.toString());
            	} else {
            		this.packets.add(packet.toXML());
1179
            	}
1180
            }
Alex Wenckus's avatar
Alex Wenckus committed
1181 1182 1183
        }

        public String getDeliverable() {
1184
            if (text == null) {
1185
                StringBuilder builder = new StringBuilder();
1186 1187
                for (String packet : packets) {
                    builder.append(packet);
1188 1189
                }
                return builder.toString();
Alex Wenckus's avatar
Alex Wenckus committed
1190 1191 1192 1193 1194
            }
            else {
                return text;
            }
        }
1195

1196
        public Collection<Packet> getPackets() {
1197 1198 1199 1200 1201
            // Check if the Deliverable is about Packets or raw XML
            if (packets == null) {
                // No packets here (should be just raw XML like <stream> so return nothing
                return null;
            }
1202
            List<Packet> answer = new ArrayList<>();
1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
            for (String packetXML : packets) {
                try {
                    Packet packet = null;
                    // Parse the XML stanza
                    Element element = localParser.get().read(new StringReader(packetXML)).getRootElement();
                    String tag = element.getName();
                    if ("message".equals(tag)) {
                        packet = new Message(element, true);
                    }
                    else if ("presence".equals(tag)) {
                        packet = new Presence(element, true);
                    }
                    else if ("iq".equals(tag)) {
                        packet = new IQ(element, true);
                    }
                    // Add the reconstructed packet to the result
                    answer.add(packet);
                }
                catch (Exception e) {
                    Log.error("Error while parsing Privacy Property", e);
                }
            }
            return answer;
        }
Alex Wenckus's avatar
Alex Wenckus committed
1227
    }
1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245

    private class Delivered {
        private long requestID;
        private Collection<Deliverable> deliverables;

        public Delivered(Collection<Deliverable> deliverables) {
            this.deliverables = deliverables;
        }

        public void setRequestID(long requestID) {
            this.requestID = requestID;
        }

        public long getRequestID() {
            return requestID;
        }

        public Collection<Packet> getPackets() {
1246
            List<Packet> packets = new ArrayList<>();
1247 1248 1249 1250 1251 1252
            synchronized (deliverables) {
	            for (Deliverable deliverable : deliverables) {
	                if (deliverable.packets != null) {
	                    packets.addAll(deliverable.getPackets());
	                }
	            }
1253 1254 1255 1256
            }
            return packets;
        }
    }
1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268

    /**
     * A runner that guarantees that the packets per a session will be sent and
     * processed in the order in which they were received.
     */
    private class HttpPacketSender implements Runnable {
        private HttpSession session;

        HttpPacketSender(HttpSession session) {
            this.session = session;
        }

1269
        @Override
1270 1271 1272 1273 1274 1275 1276 1277
        public void run() {
            session.sendPendingPackets();
        }

        private void init() {
            HttpBindManager.getInstance().getSessionManager().execute(this);
        }
    }
Alex Wenckus's avatar
Alex Wenckus committed
1278
}