-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathHttpSession.java
More file actions
1520 lines (1369 loc) · 69.1 KB
/
Copy pathHttpSession.java
File metadata and controls
1520 lines (1369 loc) · 69.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
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
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
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
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2005-2008 Jive Software, 2017-2025 Ignite Realtime Foundation. All rights reserved.
*
* 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.
*/
package org.jivesoftware.openfire.http;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.multiplex.UnknownStanzaException;
import org.jivesoftware.openfire.net.MXParser;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.VirtualConnection;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.SystemProperty;
import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.packet.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A session represents a series of interactions with an XMPP client sending packets using the HTTP
* Binding protocol specified in <a href="https://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.
*
* @author Alexander Wenckus
*/
public class HttpSession extends LocalClientSession {
private static final Logger Log = LoggerFactory.getLogger(HttpSession.class);
/**`
* Controls if client-provided 'pause' values that are invalid (higher than 'maxpause') are ignored or will cause the client to be disconnected.
*/
public static SystemProperty<Boolean> IGNORE_INVALID_PAUSE = SystemProperty.Builder.ofType(Boolean.class)
.setKey("xmpp.httpbind.client.maxpause.ignore-invalid")
.setDefaultValue(false)
.setDynamic(true)
.build();
private static XmlPullParserFactory factory = null;
private static final ThreadLocal<XMPPPacketReader> localParser;
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 = ThreadLocal.withInitial(() -> {
XMPPPacketReader parser = new XMPPPacketReader();
factory.setNamespaceAware(true);
parser.setXPPFactory(factory);
return parser;
});
}
/**
* Specifies the longest time 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.
*/
private final Duration wait;
/**
* 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".)
*/
private final int hold;
/**
* Sets whether the initial request on the session was encrypted (eg: using HTTPS instead of HTTP).
*/
private final boolean isEncrypted;
/**
* 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.
*/
private final Duration maxPollingInterval;
/**
* The max number of requests it is permissible for this session to have open at any one time.
*/
private final int maxRequests;
/**
* Sets the maximum length of a temporary session pause that the client MAY request.
*/
private final Duration maxPause;
/**
* Sets the default inactivity timeout of this session. A session's inactivity timeout can
* be temporarily changed using session pause requests.
*/
private final Duration defaultInactivityTimeout;
/**
* Returns the major version of BOSH which this session utilizes. The version refers to the
* version of the XEP which the connecting client implements.
*/
private final int majorVersion;
/**
* Sets the minor version of BOSH which the client implements.
*/
private final int minorVersion;
/**
* The X509Certificates associated with this session.
*/
private final X509Certificate[] sslCertificates;
/**
* Collection of client connections on which a BOSH request has been made, but have not been responded to.
*
* The connections in the queue will be ordered by their requestId value.
*/
@GuardedBy("itself")
private final PriorityQueue<HttpConnection> connectionQueue = new PriorityQueue<>((o1, o2) -> (int) (o1.getRequestId() - o2.getRequestId()));
/**
* A thread-safe collection (using a weakly consistent iterator) that contains stanzas that could not immediately be
* delivered to the peer.
*/
private final ConcurrentLinkedQueue<Deliverable> pendingElements = new ConcurrentLinkedQueue<>();
/**
* A list of data that has been delivered, for potential future retransmission.
*
* The size of this collection is limited. It will contain only the last few transmitted elements.
*/
@GuardedBy("itself")
private final LinkedList<Delivered> sentElements = new LinkedList<>();
private Instant lastPoll = Instant.EPOCH;
private Duration inactivityTimeout;
private Instant lastActivity;
@GuardedBy("connectionQueue")
private long lastSequentialRequestID; // received
@GuardedBy("connectionQueue")
private long lastAnsweredRequestID; // sent
private boolean lastResponseEmpty;
private final SessionPacketRouter router = new SessionPacketRouter(this);
public HttpSession(HttpVirtualConnection vConnection, String serverName,
StreamID streamID, long requestId, X509Certificate[] sslCertificates, Locale language,
Duration wait, int hold, boolean isEncrypted, Duration maxPollingInterval,
int maxRequests, Duration maxPause, Duration defaultInactivityTimeout,
int majorVersion, int minorVersion) throws UnknownHostException
{
super(serverName, vConnection, streamID, language);
this.lastActivity = Instant.now();
this.lastSequentialRequestID = requestId;
this.sslCertificates = sslCertificates;
this.wait = wait;
this.hold = hold;
this.isEncrypted = isEncrypted;
this.maxPollingInterval = maxPollingInterval;
this.maxRequests = maxRequests;
this.maxPause = maxPause;
this.defaultInactivityTimeout = defaultInactivityTimeout;
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
Log.debug("Session {} being opened with initial connection {}", getStreamID(), vConnection.toString());
}
/**
* Returns the stream features which are available for this session.
*
* @return the stream features which are available for this session.
*/
@Override
public List<Element> getAvailableStreamFeatures() {
final List<Element> elements = new ArrayList<>();
// If authentication has not happened yet, include available authentication mechanisms.
if (getAuthToken() == null) {
final List<Element> mechanisms = SASLAuthentication.getSASLMechanisms(this);
for (Element mechanism : mechanisms) {
elements.add(mechanism);
}
}
if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
elements.add(DocumentHelper.createElement(new QName("register",new Namespace("", "http://jabber.org/features/iq-register"))));
}
elements.add(DocumentHelper.createElement(new QName("bind", new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind"))));
final Element session = DocumentHelper.createElement(new QName("session", new Namespace("", "urn:ietf:params:xml:ns:xmpp-session")));
session.addElement("optional");
elements.add(session);
return elements;
}
/**
* 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.
*
* @return the longest time it is permissible to wait for a response.
*/
public Duration getWait() {
return wait;
}
/**
* 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".)
*
* @return the maximum number of simultaneous waiting requests
*/
public int getHold() {
return hold;
}
/**
* 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
* request occurs in the interval the session will be terminated.
*/
public Duration getMaxPollingInterval() {
return this.maxPollingInterval;
}
/**
* Returns the max number of requests it is permissible for this session to have open at any one
* time.
*
* @return the max number of requests it is permissible for this session to have open at any one
* time.
*/
public int getMaxRequests() {
return this.maxRequests;
}
/**
* Returns the maximum length of a temporary session pause that the client MAY request.
*
* @return the maximum length of a temporary session pause that the client MAY request.
*/
public Duration getMaxPause() {
return this.maxPause;
}
/**
* Returns true if all connections on this session should be encrypted, and false if they should
* not.
*
* @return true if all connections on this session should be encrypted, and false if they should
* not.
*/
@Override
public boolean isEncrypted() {
return isEncrypted;
}
/**
* 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.
*
* @return true if this session is a polling session.
*/
public boolean isPollingSession() {
return (this.wait.isZero() || this.hold == 0);
}
/**
* Sets the time, in seconds, after which this session will be considered inactive and be terminated.
*
* @param inactivityTimeout the time, in seconds, after which this session will be considered
* inactive and be terminated.
*/
public void setInactivityTimeout(Duration inactivityTimeout) {
this.inactivityTimeout = inactivityTimeout;
}
/**
* Resets the inactivity timeout of this session to default. A session's inactivity timeout can
* be temporarily changed using session pause requests.
*
* @see #pause(Duration)
*/
public void resetInactivityTimeout() {
this.inactivityTimeout = this.defaultInactivityTimeout;
}
/**
* 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.
*/
public Duration getInactivityTimeout() {
return inactivityTimeout;
}
/**
* 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(Duration duration) {
// Respond immediately to all pending requests
synchronized (connectionQueue) {
final Iterator<HttpConnection> iter = connectionQueue.iterator();
while (iter.hasNext()) {
final HttpConnection toClose = iter.next();
toClose.close();
iter.remove();
}
}
setInactivityTimeout(duration);
}
/**
* Returns the time in milliseconds since the epoch that this session was last active. Activity
* 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.
*
* @return the time in milliseconds since the epoch that this session was last active.
*/
public Instant getLastActivity() {
synchronized (connectionQueue) {
if (!connectionQueue.isEmpty()) {
// The session is currently active, set the last activity to the current time.
lastActivity = Instant.now();
}
return lastActivity;
}
}
/**
* 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() {
synchronized (connectionQueue) {
return lastSequentialRequestID;
}
}
/**
* 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;
}
}
/**
* 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 5 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 minor version of the BOSH XEP which the client is utilizing.
*/
public int getMinorVersion() {
if (this.minorVersion != -1) {
return this.minorVersion;
}
else {
return 5;
}
}
/**
* 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;
}
/**
* 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 body the body element that was sent containing the request for a new session.
* @param context the context of the asynchronous servlet call leading up to this method call.
*
* @throws HttpBindException for several reasons: if the encoding inside an auth packet is not recognized by the
* server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
* @throws IOException if an input or output exception occurred
*/
public void forwardRequest(HttpBindBody body, AsyncContext context)
throws HttpBindException, HttpConnectionClosedException, IOException
{
final HttpConnection connection = this.createConnection(body, context);
final long rid = body.getRid();
final StreamID streamid = getStreamID();
// Check if security restraints are observed.
if (isEncrypted && !connection.isEncrypted()) {
throw new HttpBindException("Session was started from encrypted connection, all " +
"connections on this session must be encrypted.", BoshBindingError.badRequest);
}
synchronized (connectionQueue)
{
// Check if the provided RID is in the to-be-expected window.
if (rid > (lastSequentialRequestID + maxRequests)) {
Log.warn("Request {} > {}, ending session {}", body.getRid(), (lastSequentialRequestID + maxRequests), getStreamID());
throw new HttpBindException("Unexpected RID error.", BoshBindingError.itemNotFound);
}
// Check for retransmission.
if (rid <= lastAnsweredRequestID) {
Log.debug("Request {} on session {} appears to be a request for redelivery, as the last answered RID is {}", rid, getStreamID(), lastAnsweredRequestID);
redeliver(connection);
return;
}
/*
* 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.
*/
final Iterator<HttpConnection> iter = connectionQueue.iterator();
while (iter.hasNext()) {
final HttpConnection queuedConnection = iter.next();
if (queuedConnection.getRequestId() == rid) {
Log.debug("Found previous connection in queue with rid {}", rid);
// Note that the old connection is removed here, but the new connection will not be added back before the mutex is released. This leaves
// room for another thread to try and consume a connection before this connection has been restored. This should be safe, as the consumer
// should not be allowed to consume a connection with a RID that, compared to the previously consumed RID, leaves a 'gap'. In that case,
// the to-be-delivered data is expected to be queued, which will be picked up as soon as this connection (for which the RID 'fills the gap',
// is being processed by org.jivesoftware.openfire.http.HttpSession.processConnection.
iter.remove();
assert !queuedConnection.isClosed();
Log.debug("For session {} queued connection is still open - calling close() on the old connection (as the new connection will replace it).", streamid);
// TODO: OF-2447: implement section 14.3 of XEP 0124 instead of this!
deliver(queuedConnection, Collections.singletonList(new Deliverable("")), true);
queuedConnection.close();
break;
}
}
}
checkOveractivity(connection);
// Process the client's 'ack' attribute (XEP-0124 §9.2): clean up sentElements that have been acknowledged.
final Long clientAck = body.getAck();
if (clientAck != null) {
synchronized (sentElements) {
sentElements.removeIf(delivered -> delivered.getRequestID() <= clientAck);
}
Log.trace("Session {}: client acknowledged responses up to rid {}; cleaned up sentElements buffer.", getStreamID(), clientAck);
}
// Schedule the connection for consumption.
processConnection(connection, context);
resetInactivityTimeout();
}
/**
* This method sends any pending packets in the session. If no packets are
* pending, this method simply returns. The method is internally synchronized
* to avoid simultaneous sending operations on this Session. If two
* threads try to run this method simultaneously, the first one will trigger
* the pending packets to be sent, while the second one will simply return
* (as there are no packets left to send).
*/
protected void sendPendingPackets(final List<Element> packetsToSend) {
if (packetsToSend == null || packetsToSend.isEmpty()) {
return;
}
// Schedule in-order.
synchronized (router) {
HttpBindManager.getInstance().getSessionManager().execute(this, () -> {
Log.trace("Stream {}: sending {} packet(s)", streamID, packetsToSend.size());
for (Element packet : packetsToSend) {
try {
router.route(packet);
} catch (UnknownStanzaException e) {
Log.error("On session {} client provided unknown packet type: {}", getStreamID(), packet.asXML(), e);
}
}
});
}
}
/**
* Return the X509Certificates associated with this session.
*
* @return the X509Certificate associated with this session.
*/
@Override
public X509Certificate[] getPeerCertificates() {
return sslCertificates;
}
/**
* Creates a new connection on this session.
*
* @param body the body element that was sent containing the request for a new session.
* @param context the context of the asynchronous servlet call leading up to this method call.
* @return the created {@link HttpConnection} which represents the connection.
*/
@Nonnull
synchronized HttpConnection createConnection(@Nonnull final HttpBindBody body, @Nonnull final AsyncContext context)
{
final HttpConnection connection = new HttpConnection(body, context);
final StreamID streamID = getStreamID();
final long rid = body.getRid();
Log.debug( "Creating connection for rid: {} in session {}", rid, streamID );
connection.setSession(this);
context.setTimeout(getWait().toMillis());
context.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) {
Log.trace("Session {} Request ID {}, event complete: {}", streamID, rid, asyncEvent);
synchronized (connectionQueue) {
if (connectionQueue.remove(connection) || !connection.isClosed()) {
Log.warn("Discovered a 'complete' event for a BOSH connection that has not been consumed (for session {} with Request ID {}, was closed: {}). This likely is a bug in Openfire.", streamID, rid, connection.isClosed());
}
}
lastActivity = Instant.now();
SessionEventDispatcher.dispatchEvent( HttpSession.this, SessionEventDispatcher.EventType.connection_closed, connection, context );
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
Log.trace("Session {} Request ID {}, event timeout: {}. Returning an empty response.", streamID, rid, asyncEvent);
try {
// 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.
synchronized (connectionQueue) {
// Consume the connection that is timing out, by removing it from the queue and sending data to it.
connectionQueue.remove(connection);
deliver(connection, Collections.singletonList(new Deliverable("")), false);
setLastResponseEmpty(true);
}
} catch (HttpConnectionClosedException e) {
Log.warn("Unexpected exception while processing connection timeout.", e);
}
// Note that 'onComplete' will be invoked.
}
@Override
public void onError(AsyncEvent asyncEvent) {
Log.trace("Session {} Request ID {}, event error: {}", streamID, rid, asyncEvent);
Log.warn("For session {} an unhandled AsyncListener error occurred: ", streamID, asyncEvent.getThrowable());
synchronized (connectionQueue) {
// There was an error with a connection. Make sure it cannot be consumed again.
connectionQueue.remove(connection);
}
SessionEventDispatcher.dispatchEvent( HttpSession.this, SessionEventDispatcher.EventType.connection_closed, connection, context );
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) {
Log.trace("Session {} Request ID {}, event start: {}", streamID, rid, asyncEvent);
lastActivity = Instant.now();
}
});
return connection;
}
/**
* Attempts to find data that was previously sent back to the client, using a particular request ID. This is
* expected to be used to process requests for retransmission.
*
* The implementation only holds a limited amount of data. It will return an empty Optional when no data that
* matches the ID can be found.
*
* @param rid The request ID for which to find previously delivered data.
* @return previously delivered data when found.
*/
@Nonnull
private Optional<Delivered> retrieveDeliverable(final long rid) {
synchronized (sentElements) {
for (Delivered delivered : sentElements) {
if (delivered.getRequestID() == rid) {
return Optional.of(delivered);
}
}
}
return Optional.empty();
}
/**
* Schedules the connection for processing, ensuring that connections are processed in the order of their request ID
* values. Processing causes data delivered by the client to be routed in the server to their intended recipients,
* and allow the connection to be used to send back data to the client.
*
* @param connection The connection ready to be processed.
* @param context the context of the asynchronous servlet call leading up to this method call.
*/
private void processConnection(@Nonnull final HttpConnection connection, @Nonnull final AsyncContext context) throws HttpConnectionClosedException, IOException
{
final long rid = connection.getRequestId();
final StreamID streamid = getStreamID();
Log.debug( "Adding connection to stream {} with rid {}", streamid, rid );
// Note that connections can be expected to arrive 'out of order'. The implementation should only use a connection
// that has a request ID value that's exactly one higher than the last request ID value in the 'gap-less' sequence
// of request IDs. When a connection is being processed that has a higher value, it should go unused until another
// connection arrives that 'fills the gap' (and be used only _after_ that connection gets used).
boolean aConnectionAvailableForDelivery = false;
boolean mustClose = false;
synchronized (connectionQueue) {
// Note that this queue will automatically order its entities.
connectionQueue.add(connection);
lastActivity = Instant.now();
final Iterator<HttpConnection> iter = connectionQueue.iterator();
while (iter.hasNext()) {
final HttpConnection queuedConnection = iter.next();
assert !queuedConnection.isClosed();
final long queuedRequestID = queuedConnection.getRequestId();
if (queuedRequestID <= lastSequentialRequestID)
{
// The request body (inbound data) for this request will already have been processed, but the connection remains queued, waiting to be used to send outbound data.
Log.trace("Detected a queued connection (for session {}) with a request ID ({}) that is not higher than the last sequential request ID ({}). This connection is waiting to be used to deliver outbound data back to the client.", streamid, queuedRequestID, lastSequentialRequestID);
continue;
}
if (queuedRequestID == lastSequentialRequestID + 1)
{
Log.debug("Detected a queued connection (for session {}) with request ID ({}) that is exactly one higher than the last sequential request ID ({}). This inbound data on this connection will now be processed, after which the connection can be used to send outbound data back to the client.", streamid, queuedRequestID, lastSequentialRequestID);
// The data that was provided by the client can now be processed by the server.
// The below sends this data asynchronously.
sendPendingPackets(queuedConnection.getInboundDataQueue());
// Evaluate edge-cases.
if (queuedConnection.isTerminate()) {
Log.debug("Connection (for session {}) with request ID ({}) is a request to terminate.", getStreamID(), queuedRequestID);
iter.remove(); // This connection will be consumed here.
queuedConnection.deliverBody(createEmptyBody(true, queuedRequestID), true);
mustClose = true;
} else if (queuedConnection.isRestart()) {
Log.debug("Connection (for session {}) with request ID ({}) is a request to restart.", getStreamID(), queuedRequestID);
iter.remove(); // This connection has now been fully consumed.
queuedConnection.deliverBody(createSessionRestartResponse(), true);
} else if (queuedConnection.getPause() != null) {
// OF-2449: Error when the requested pause is higher than the allowed maximum.
if (!IGNORE_INVALID_PAUSE.getValue() && (queuedConnection.getPause().compareTo(getMaxPause()) > 0 || queuedConnection.getPause().isNegative())) {
Log.info("Connection (for session {}) with request ID ({}) is a request to pause (for {}) that is outside of the permissible range of 0 to {}", getStreamID(), queuedRequestID, queuedConnection.getPause(), getMaxPause());
queuedConnection.deliverBody(createTerminalBindingBody("policy-violation"), true);
mustClose = true;
} else {
Log.debug("Connection (for session {}) with request ID ({}) is a request to pause (for {}).", getStreamID(), queuedRequestID, queuedConnection.getPause());
pause(queuedConnection.getPause());
queuedConnection.deliverBody(createEmptyBody(false, queuedRequestID), true);
setLastResponseEmpty(true);
}
iter.remove(); // This connection will be consumed by this block. It should not be processed by the 'pause' method.
} else {
// At least one new connection has become available, and can be used to return data to the client.
aConnectionAvailableForDelivery = true;
}
// There is a new connection available for consumption.
lastSequentialRequestID = queuedRequestID;
// Note that when this connection fills a gap in the sequence, other connections might already be
// available that have the 'next' Request ID value. We need to keep iterating.
} else {
// As we're iterating over the collection that is ordered by Request ID, the iteration can stop
// when a gap is detected (subsequent connections will only have higher Request ID values).
break;
}
}
if (!mustClose) {
// If a connection became available for delivery and there's pending data to be delivered, deliver immediately.
// Request ID of the new connection 'fits in the window'
if (isPollingSession()) {
// Note that the code leading up to here checks if the Request ID of the new connection 'fits in the window',
// which means that for polling sessions, the request ID must have been a sequential one, which in turn should
// guarantee that 'a new connection is now available for delivery').
assert aConnectionAvailableForDelivery; // FIXME: OF-2451: the edge-cases evaluated above make this assertion not necessarily true.
}
if (isPollingSession() || aConnectionAvailableForDelivery) {
SessionEventDispatcher.dispatchEvent(this, SessionEventDispatcher.EventType.connection_opened, connection, context); // TODO is this the right place to dispatch this event?
tryImmediateDelivery();
}
// When a new connection has become available, older connections need to be released (allowing the client to
// send more data if it needs to).
while (!connectionQueue.isEmpty() && connectionQueue.size() > hold) {
Log.trace("Stream {}: releasing oldest connection (rid {}), as the amount of open connections ({}) is higher than the requested amount to hold ({}).", streamid, rid, connectionQueue.size(), hold);
final HttpConnection openConnection = connectionQueue.peek();
assert openConnection != null;
if (openConnection.getRequestId() > lastSequentialRequestID) {
break; // There's a gap. As described above, connections must be used in sequence, without jumping the queue.
}
// Consume this connection.
connectionQueue.poll();
openConnection.deliverBody(createEmptyBody(false, openConnection.getRequestId()), true);
}
}
}
// OF-2444: Call 'close()' outside of the connectionQueue mutex, to avoid deadlocks.
if (mustClose) {
close();
}
}
@Override
public void close()
{
// Make the worker pool process this, to help ensure that the 'close' event is being processed in order (eg: not prior to the processing of other data, such as any pending processing of data).
synchronized (router) {
try {
HttpBindManager.getInstance().getSessionManager().execute(this, () -> {
Log.trace("Stream {}: Closing", streamID);
super.markNonResumable();
super.close();
});
} catch (Throwable t) {
Log.warn("Unable to close session", t);
}
}
}
/**
* Attempts to process a request for redelivery of data that was sent earlier.
*
* This method is expected to be called with a connection that has a request ID that was already processed before.
*
* If the data that was delivered in the earlier connection is still available, this will be delivered on this new
* connection again. When the data is no longer available, an item-not-found error is generated.
*
* @param connection The connection requesting a retransmission
* @throws HttpConnectionClosedException if the connection was closed before a response could be delivered.
* @throws HttpBindException if the connection has violated a facet of the HTTP binding protocol.
*/
@GuardedBy("connectionQueue")
private void redeliver(@Nonnull final HttpConnection connection) throws HttpBindException, IOException, HttpConnectionClosedException
{
Log.debug("Session {} requesting a retransmission for rid {}", getStreamID(), connection.getRequestId());
final Optional<Delivered> deliverable = retrieveDeliverable(connection.getRequestId());
if (deliverable.isEmpty()) {
Log.warn("Deliverable unavailable for {} in session {}", connection.getRequestId(), getStreamID());
throw new HttpBindException("Unexpected RID error.", BoshBindingError.itemNotFound);
}
connection.deliverBody(asBodyText(deliverable.get().deliverables, connection.getRequestId()), true);
}
private enum OveractivityType {
NONE,
TOO_MANY_SIM_REQS,
POLLING_TOO_QUICK
}
/**
* 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.
*
* @see <a href="http://www.xmpp.org/extensions/xep-0124.html#overactive">overactive</a>
* @param connection the new connection.
* @throws HttpBindException if the connection has violated a facet of the HTTP binding
* protocol.
*/
private void checkOveractivity(HttpConnection connection) throws HttpBindException {
int pendingConnections;
OveractivityType overactivity = OveractivityType.NONE;
synchronized (connectionQueue) {
pendingConnections = connectionQueue.size();
}
Instant time = Instant.now();
Duration deltaFromLastPoll = Duration.between(lastPoll, time).abs();
// XEP-0124 §11: The client MAY make one additional request if it is to pause or terminate a session.
final boolean isPauseOrTerminate = connection.isTerminate() || connection.getPause() != null;
final int maxSimultaneousRequests = isPauseOrTerminate ? maxRequests + 1 : maxRequests;
if(pendingConnections >= maxSimultaneousRequests) {
overactivity = OveractivityType.TOO_MANY_SIM_REQS;
}
else if(connection.isPoll()) {
boolean localIsPollingSession = isPollingSession();
if (deltaFromLastPoll.compareTo(maxPollingInterval) < 0) {
if (localIsPollingSession) {
overactivity = lastResponseEmpty ? OveractivityType.POLLING_TOO_QUICK : OveractivityType.NONE;
} else {
overactivity = pendingConnections >= maxRequests ? OveractivityType.POLLING_TOO_QUICK : OveractivityType.NONE;
}
}
lastPoll = time;
Log.debug("Updated session {} lastPoll to {} with rid {} lastResponseEmpty = {} overactivity = {} deltaFromLastPoll = {} isPollingSession() = {} maxRequests = {} pendingConnections = {}",
getStreamID(), lastPoll, connection.getRequestId(), lastResponseEmpty, overactivity, deltaFromLastPoll, localIsPollingSession, maxRequests, pendingConnections);
}
setLastResponseEmpty(false);
if( overactivity != OveractivityType.NONE) {
StringBuilder errorMessage = new StringBuilder("Overactivity detected");
switch (overactivity) {
case TOO_MANY_SIM_REQS: {
errorMessage.append(", too many simultaneous requests.");
break;
}
case POLLING_TOO_QUICK: {
errorMessage.append(", minimum polling interval is ");
errorMessage.append(maxPollingInterval);
errorMessage.append(", current session ");
errorMessage.append(" interval ");
errorMessage.append(deltaFromLastPoll);
break;
}
default: {
throw new HttpBindException("Unhandled overactivity type: " + overactivity, BoshBindingError.internalServerError);
}
}
String errorMessageStr = errorMessage.toString();
Log.debug(errorMessageStr);
if (!JiveGlobals.getBooleanProperty("xmpp.httpbind.client.requests.ignoreOveractivity", false)) {
throw new HttpBindException(errorMessageStr, BoshBindingError.policyViolation);
}
}
}
@Override
public void deliver(@Nonnull final Packet stanza) {
deliver(new Deliverable(Collections.singletonList(stanza)));
}
/**
* Delivers the provided data to the client, using the first available
* connection, queuing the data of no connection is presently available.
*
* The data is expected to be XMPP-valid.
*
* @param stanza data to be delivered.
*/
private void deliver(@Nonnull final Deliverable stanza) {
deliver(Collections.singletonList(stanza));
}
/**
* Delivers the provided data to the client, using the first available
* connection, queuing the data of no connection is presently available.
*
* The data is expected to be XMPP-valid.
*
* @param deliverable data to be delivered.
*/
private void deliver(@Nonnull final List<Deliverable> deliverable)
{
pendingElements.addAll(deliverable); // ConcurrentLinkedQueue.addAll is not guaranteed to be atomic. We don't need that, as long as the insertion/iteration order is guaranteed. I'm not sure if it is (but I assume so).
tryImmediateDelivery();
}
/**
* Tries to deliver all data that is queued to be sent to the client, if data has been queued and a connection is
* available for delivery.
*
* When no data is queued for delivery, or when no connection is available, an invocation does nothing.
*/
private void tryImmediateDelivery()
{
final List<Deliverable> deliverables = drainPendingElements();
if (deliverables.isEmpty()) {
Log.trace("Immediate delivery of pending data to the client on session {} was requested, but no data is pending.", getStreamID());
return;
}
// OF-2445: stanzas should not linger forever in a queue of a session that is already closed.
if (isClosed()) {
deliverables.forEach(deliverable -> failDelivery(deliverable.getPackets()));
return;
}
final Optional<HttpConnection> connection = getConnectionReadyForOutboundDelivery();
if (connection.isEmpty()) {
Log.trace("Immediate delivery of pending data to the client on session {} was requested, but no connection is available. The data ({} deliverables) will be re-queued.", getStreamID(), deliverables.size());
// place pending deliverables back on queue. // FIXME: if other threads have placed pending elements, this will cause a re-order, which might be undesirable.
pendingElements.addAll(deliverables);
return;
}
// OF-2444: deliver asynchronously, to avoid deadlocking issues.
HttpBindManager.getInstance().getSessionManager().execute(this, () -> {
try {
Log.trace("Stream {}: Immediate delivery of {} deliverable(s)", streamID, deliverables.size());
deliver(connection.get(), deliverables, true);
} catch (HttpConnectionClosedException e) {
/* Connection was closed, try the next one. Indicates a (concurrency?) bug. */
Log.warn("Iterating over a connection that was closed for session {}. Openfire will recover from this problem, but it should not occur in the first place.", getStreamID(), e);
// place pending deliverables back on queue. // FIXME: if other threads have placed pending elements, this will cause a re-order, which might be undesirable.
pendingElements.addAll(deliverables);
} catch (IOException e) {
Log.warn("An unexpected exception occurred while iterating over connections for session {}. Openfire will attempt to recover by ignoring this connection.", getStreamID(), e);
// place pending deliverables back on queue. // FIXME: if other threads have placed pending elements, this will cause a re-order, which might be undesirable.
pendingElements.addAll(deliverables);
}
});
}
/**
* Returns a connection that can be used to deliver data to the client, if such a connection is currently available.
*
* @return A connection that is ready to be responded to.
*/