2using System.Collections.Generic;
4using System.Net.Security;
6using System.Threading.Tasks;
27 private readonly LinkedList<OutputRec> outputQueue =
new LinkedList<OutputRec>();
28 private readonly LinkedList<(long, XmlElement, Stream, string)> inputQueue =
new LinkedList<(
long, XmlElement, Stream,
string)>();
31 private DateTime maxRidTimestamp = DateTime.MinValue;
32 private DateTime firstEmpty = DateTime.MinValue;
33 private Stream currentStream =
null;
34 private readonly
string sid;
37 private readonly
string language;
38 private string key =
null;
39 private string waitingEcho =
null;
40 private readonly
double version;
41 private long? waitingRid =
null;
42 private long? lastRid =
null;
43 private long maxRid = -1;
44 private readonly
int hold;
45 private readonly
int waitSeconds;
46 private readonly
int pollingSeconds;
47 private readonly
int requests;
48 private int nrEmpty = 0;
49 private readonly
bool ack;
50 private readonly
bool secure;
51 private bool terminated =
false;
52 private string terminationCondition =
null;
54 private readonly
string remoteEndpoint =
string.
Empty;
55 private DateTime nextTimeout = DateTime.MinValue;
56 private bool removed =
false;
87 this.webResource = WebResource;
104 private class OutputRec
106 public string Payload;
107 public EventHandlerAsync<DeliveryEventArgs> Callback;
119 public string Sid => this.sid;
164 public bool Ack => this.ack;
176 get => this.terminated;
177 internal set => this.terminated = value;
186 internal set => this.isBound = value;
195 internal set => this.key = value;
198 internal bool Removed
201 set => this.removed = value;
225 this.isBound =
false;
226 await this.ProcessFragment(
"<presence type=\"unavailable\" xmlns=\"jabber:client\"/>",
null);
227 this.
Server?.ConnectionClosed(
this);
230 if (!(this.waiting is
null))
232 await this.
Error(
"Connection disposed.");
233 await this.webResource.BoshError(this.waiting,
string.Empty);
235 this.waitingRid =
null;
236 this.waitingEcho =
null;
239 this.terminated =
true;
247 this.syncObjOutput?.Dispose();
248 this.syncObjOutput =
null;
251 this.syncObjInput =
null;
253 await base.DisposeAsync();
261 await this.
Error(this.terminationCondition);
262 await this.webResource.BoshError(Response, this.terminationCondition);
266 LinkedList<OutputRec> ToCall =
null;
267 StringBuilder Xml =
null;
268 bool RemoveTimeout =
false;
269 bool ScheduleTimeout =
false;
271 if (!await this.syncObjOutput.TryBeginWrite(10000))
272 throw new TimeoutException(
"Unable to get access to session.");
276 if (Rid.HasValue && Rid.Value <=
this.maxRid)
278 await this.EmptyResponseLocked(Response, Rid, Echo);
282 if (!(this.waiting is
null))
284 if (this.waitingRid.HasValue && Rid.HasValue && Rid.Value <
this.waitingRid.Value)
286 await this.EmptyResponseLocked(Response, Rid, Echo);
292 await this.EmptyResponseLocked(this.waiting, this.waitingRid, this.waitingEcho);
299 RemoveTimeout =
true;
301 this.waitingRid =
null;
302 this.waitingEcho =
null;
305 if (this.outputQueue.First is
null)
307 this.waitingRid = Rid;
308 this.waiting = Response;
309 this.waitingEcho = Echo;
310 ScheduleTimeout =
true;
315 Xml =
new StringBuilder();
319 Xml.Append(
"<body from='");
324 Xml.Append(
"' ack='");
325 Xml.Append(Rid.Value.ToString());
326 this.RidReturnedLocked(Rid.Value);
329 if (!
string.IsNullOrEmpty(Echo))
331 Xml.Append(
"' echo='");
335 Xml.Append(
"' xmlns='");
336 Xml.Append(BoshWebClientResource.HttpBindNamespace);
340 Xml.Append(
"' xmlns:stream='");
346 foreach (OutputRec Fragment
in this.outputQueue)
348 Xml.Append(Fragment.Payload);
350 if (!(Fragment.Callback is
null))
353 ToCall =
new LinkedList<OutputRec>();
355 ToCall.AddLast(Fragment);
359 Xml.Append(
"</body>");
361 this.outputQueue.Clear();
363 string Tx = Xml.ToString();
364 await this.webResource.Return(Response, Tx);
370 await this.syncObjOutput.EndWrite();
373 if ((RemoveTimeout || ScheduleTimeout) && this.nextTimeout != DateTime.MinValue)
376 this.nextTimeout = DateTime.MinValue;
380 this.nextTimeout =
Scheduler.
Add(DateTime.Now.AddSeconds(
this.waitSeconds),
this.Timeout,
null);
382 if (!(ToCall is
null))
384 foreach (OutputRec Rec
in ToCall)
389 private async Task EmptyResponseLocked(
HttpResponse Response,
long? Rid,
string Echo)
391 StringBuilder Xml =
new StringBuilder();
393 Xml.Append(
"<body from='");
398 Xml.Append(
"' ack='");
399 Xml.Append(Rid.ToString());
400 this.RidReturnedLocked(Rid.Value);
403 if (!
string.IsNullOrEmpty(Echo))
405 Xml.Append(
"' echo='");
409 Xml.Append(
"' xmlns='");
410 Xml.Append(BoshWebClientResource.HttpBindNamespace);
413 string Tx = Xml.ToString();
414 await this.webResource.Return(Response, Tx);
418 private async
void Timeout(
object P)
424 LinkedList<OutputRec> ToCall =
null;
428 if (this.
disposed || this.nextTimeout == DateTime.MinValue)
431 this.nextTimeout = DateTime.MinValue;
435 if (!await this.syncObjOutput.TryBeginWrite(10000))
436 throw new TimeoutException(
"Unable to get access to session.");
440 if (this.waiting is
null)
443 Response = this.waiting;
444 Rid = this.waitingRid;
445 Echo = this.waitingEcho;
447 this.waitingRid =
null;
448 this.waitingEcho =
null;
450 Xml =
new StringBuilder();
452 Xml.Append(
"<body from='");
457 Xml.Append(
"' ack='");
458 Xml.Append(Rid.Value.ToString());
459 this.RidReturnedLocked(Rid.Value);
462 if (!
string.IsNullOrEmpty(Echo))
464 Xml.Append(
"' echo='");
468 Xml.Append(
"' xmlns='");
469 Xml.Append(BoshWebClientResource.HttpBindNamespace);
473 Xml.Append(
"' xmlns:stream='");
474 Xml.Append(XmppClientConnection.StreamNamespace);
477 if (this.outputQueue.First is
null)
483 foreach (OutputRec Fragment
in this.outputQueue)
485 Xml.Append(Fragment);
487 if (!(Fragment.Callback is
null))
489 if (!(ToCall is
null))
490 ToCall =
new LinkedList<OutputRec>();
492 ToCall.AddLast(Fragment);
496 Xml.Append(
"</body>");
498 this.outputQueue.Clear();
503 await this.syncObjOutput.EndWrite();
506 string Tx = Xml.ToString();
507 await this.webResource.Return(Response, Tx);
516 if (!(ToCall is
null))
518 foreach (OutputRec Rec
in ToCall)
535 public override async Task<bool>
BeginWrite(
string Xml, EventHandlerAsync<DeliveryEventArgs> Callback,
object State)
540 if (
string.IsNullOrEmpty(Xml))
545 DateTime Now = DateTime.Now;
551 if (!await this.syncObjOutput.TryBeginWrite(10000))
552 throw new TimeoutException(
"Unable to get access to session.");
556 if (this.waiting is
null)
558 this.outputQueue.AddLast(
new OutputRec()
567 Response = this.waiting;
568 Rid = this.waitingRid;
569 Echo = this.waitingEcho;
571 this.waitingRid =
null;
572 this.waitingEcho =
null;
576 await this.syncObjOutput.EndWrite();
581 StringBuilder Xml2 =
new StringBuilder();
583 Xml2.Append(
"<body from='");
588 Xml2.Append(
"' ack='");
589 Xml2.Append(Rid.Value.ToString());
590 this.RidReturnedLocked(Rid.Value);
593 if (!
string.IsNullOrEmpty(Echo))
595 Xml2.Append(
"' echo='");
599 Xml2.Append(
"' xmlns='");
604 Xml2.Append(
"' xmlns:stream='");
610 Xml2.Append(
"</body>");
612 string Tx = Xml2.ToString();
613 await this.webResource.Return(Response, Tx);
635 public override Task<bool>
StreamError(
string ErrorXml,
string Reason)
637 return this.ToError(
"<stream:error>" + ErrorXml +
"</stream:error>", Reason);
640 private async Task<bool> ToError(
string ErrorXml,
string Reason)
642 this.terminated =
true;
648 if (
string.IsNullOrEmpty(ErrorXml))
655 return await this.
BeginWrite(ErrorXml, async (Sender, e) =>
662 private async Task<bool> ProcessFragment(
string Xml, Stream Stream)
668 if (!
string.IsNullOrEmpty(this.
fullJid))
671 Doc =
new XmlDocument()
673 PreserveWhitespace =
true
675 Doc.LoadXml(
"<body xmlns='" + BoshWebClientResource.HttpBindNamespace +
"'>" + Xml +
"</body>");
677 return await this.ProcessStanzas(Doc.DocumentElement, Stream);
682 await this.
StreamError(
"<bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>", ex.Message);
688 internal async Task<bool> ProcessStanzasInOrder(
long Rid, XmlElement RootElement, Stream Stream,
string Key)
690 bool Process =
false;
696 throw new TimeoutException(
"Unable to get access to session.");
700 if (!this.lastRid.HasValue || Rid ==
this.lastRid.Value + 1)
702 if (!this.CheckNextKey(
Key))
710 if (this.inputQueue.Count >=
this.hold || (Rid -
this.lastRid.Value) >
this.requests)
712 this.terminated =
true;
713 this.terminationCondition =
"item-not-found";
716 this.inputQueue.AddLast((Rid, RootElement, Stream,
Key));
727 Log.
Error(
"Terminating session. Too many incoming requests.", this.
fullJid);
730 bool FirstElement =
true;
735 FirstElement =
false;
737 XmlElement RootElement0 = RootElement;
738 Stream Stream0 = Stream;
740 LinkedListNode<(long, XmlElement, Stream, string)> First;
741 (long, XmlElement, Stream, string) Rec;
744 throw new TimeoutException(
"Unable to get access to session.");
748 if (!((First = this.inputQueue.First) is
null) &&
749 (Rec = First.Value).Item1 == Rid + 1)
753 RootElement = Rec.Item2;
756 this.inputQueue.RemoveFirst();
758 if (this.CheckNextKey(
Key))
763 while (RootElement is
null &&
764 !((First = this.inputQueue.First) is
null) &&
765 (Rec = First.Value).Item1 == Rid + 1);
767 if (!(RootElement is
null))
780 if (!await this.ProcessStanzas(RootElement0, Stream0))
788 while (!(RootElement is
null));
794 internal bool CheckNextKey(
string Key)
798 if (!(this.key is
null))
804 int i, c = PrevKey.Length;
806 if (c != this.key.Length)
809 for (i = 0; i < c; i++)
811 if (PrevKey[i] != this.key[i])
827 return this.currentStream as SslStream;
830 internal async Task<bool> ProcessStanzas(XmlElement RootElement, Stream Stream)
839 foreach (XmlNode N
in RootElement.ChildNodes)
841 if (N is XmlElement E)
849 this.currentStream = Stream;
855 await this.Exception(ex);
856 if (!await this.
StreamError(
"<bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>", ex.Message))
866 StringBuilder Xml =
new StringBuilder();
867 await this.InitStream(Xml, Stream as SslStream);
868 if (!await this.
BeginWrite(Xml.ToString(),
null,
null))
873 DateTime Now = DateTime.Now;
875 if (this.nrEmpty == 0)
876 this.firstEmpty = Now;
879 if (this.nrEmpty > 5)
881 double SecondsPerRequest = (Now - this.firstEmpty).TotalSeconds / this.nrEmpty;
883 if (SecondsPerRequest * 2 < this.pollingSeconds)
885 this.terminated =
true;
886 this.terminationCondition =
"policy-violation";
887 BoshWebClientResource.Remove(
this);
913 return await this.
StreamError(
"<unsupported-stanza-type xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>",
914 "Unrecognized stanza: " + StanzaElement.LocalName);
917 internal async Task InitStream(StringBuilder sb, SslStream SslStream)
919 sb.Append(
"<stream:features>");
929 if (Mechanism.
Allowed(SslStream))
931 sb.Append(
"<mechanism>");
932 sb.Append(Mechanism.
Name);
933 sb.Append(
"</mechanism>");
937 sb.Append(
"</mechanisms>");
939 if (await this.
server.CanRegister(
this))
940 sb.Append(
"<register xmlns='http://jabber.org/features/iq-register'/>");
946 sb.Append(
"<bind xmlns='" + XmppClientConnection.BindNamespace +
"'/>");
947 sb.Append(
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>");
950 sb.Append(
"</stream:features>");
953 internal void RidReturnedLocked(
long Rid)
955 if (Rid > this.maxRid)
958 this.maxRidTimestamp = DateTime.Now;
968 return !this.disposed && !this.removed && this.State != XmppConnectionState.Error && this.State !=
XmppConnectionState.Offline;
Helps with common XML-related tasks.
static string Encode(string s)
Encodes a string for use in XML.
Static class managing the application event log. Applications and services log events on this static ...
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
static void Error(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an error event.
Task Error(string Error)
Called to inform the viewer of an error state.
Task Exception(Exception Exception)
Called to inform the viewer of an exception state.
ISniffer[] Sniffers
Registered sniffers.
Task TransmitText(string Text)
Called when text has been transmitted.
bool HasSniffers
If there are sniffers registered on the object.
Task ReceiveText(string Text)
Called when text has been received.
Event arguments for delivery events.
Represets a response of an HTTP client request.
CaseInsensitiveString From
From address
override SslStream GetSslStream()
Returns the underlying encrypted stream.
double Version
BOSH protocol version
bool Terminated
If the session has been terminated.
override string Binding
Binding method.
int PollingSeconds
How rapidly resource can be polled (in seconds).
override string Protocol
String representing protocol being used.
bool IsBound
If the session is bound.
BoshSession(BoshWebClientResource WebResource, string Sid, long? Rid, double Version, CaseInsensitiveString From, CaseInsensitiveString To, string Language, int Hold, int WaitSeconds, int PollingSeconds, int Requests, bool Ack, bool Secure, string RemoteEndpoint, string Key, XmppServer Server, params ISniffer[] Sniffers)
BOSH Session
override Task< bool > StreamError(string ErrorXml, string Reason)
Returns a stream error.
override bool CheckLive()
Checks if the connection is live.
int WaitSeconds
Maximum time (in seconds) to wait.
override async Task DisposeAsync()
IDisposable.Dispose
override string RemoteEndpoint
Remote endpoint.
CaseInsensitiveString To
To address (domain)
bool Secure
If connection is secure.
string Language
Default language
bool Ack
If requests are acknowledged.
override async Task< bool > BeginWrite(string Xml, EventHandlerAsync< DeliveryEventArgs > Callback, object State)
Starts sending an XML fragment to the client.
int Requests
Number of simultaneous requests open.
override async Task< bool > ProcessBindingSpecificStanza(Stanza Stanza, XmlElement StanzaElement)
Processes a binding-specific stanza.
const string HttpBindNamespace
http://jabber.org/protocol/httpbind
Abstract base class for XMPP client connections
bool isAuthenticated
If user is authenticated
XmppConnectionState State
Current state of connection.
async Task< bool > ProcessStanza(Stanza Stanza)
Processes an XMPP Stanza.
bool isBound
If user is bound
const string StreamNamespace
http://etherx.jabber.org/streams
CaseInsensitiveString FullJid
Full JID
bool disposed
If connection is disposed
XmppServer Server
XMPP Server serving the client.
CaseInsensitiveString fullJid
Full JID
XmppServer server
XMPP Server
Contains information about a stanza.
Class managing a connection.
const string SaslNamespace
urn:ietf:params:xml:ns:xmpp-sasl
Represents a case-insensitive string.
static readonly CaseInsensitiveString Empty
Empty case-insensitive string
Represents an object that allows single concurrent writers but multiple concurrent readers....
virtual Task EndWrite()
Ends a writing session of the object. Must be called once for each call to BeginWrite or successful c...
virtual async Task< bool > TryBeginWrite(int Timeout)
Waits, at most Timeout milliseconds, until object ready for writing. Each successful call to TryBegi...
virtual void Dispose()
IDisposable.Dispose
Class that can be used to schedule events in time. It uses a timer to execute tasks at the appointed ...
bool Remove(DateTime When)
Removes an event scheduled for a given point in time.
DateTime Add(DateTime When, ScheduledEventCallback Callback, object State)
Adds an event.
Contains methods for simple hash calculations.
static string ComputeSHA1HashString(byte[] Data)
Computes the SHA-1 hash of a block of binary data.
Interface for authentication mechanisms.
bool Allowed(SslStream SslStream)
Checks if a mechanism is allowed during the current conditions.
string Name
Name of the mechanism.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...
XmppConnectionState
State of XMPP connection.