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 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 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))
352 ToCall ??=
new LinkedList<OutputRec>();
353 ToCall.AddLast(Fragment);
357 Xml.Append(
"</body>");
359 this.outputQueue.Clear();
361 string Tx = Xml.ToString();
362 await this.webResource.Return(Response, Tx);
368 await this.syncObjOutput.EndWrite();
371 if ((RemoveTimeout || ScheduleTimeout) && this.nextTimeout != DateTime.MinValue)
374 this.nextTimeout = DateTime.MinValue;
378 this.nextTimeout =
Scheduler.
Add(DateTime.Now.AddSeconds(
this.waitSeconds),
this.Timeout,
null);
380 if (!(ToCall is
null))
382 foreach (OutputRec Rec
in ToCall)
387 private async Task EmptyResponseLocked(
HttpResponse Response,
long? Rid,
string Echo)
389 StringBuilder Xml =
new StringBuilder();
391 Xml.Append(
"<body from='");
396 Xml.Append(
"' ack='");
397 Xml.Append(Rid.ToString());
398 this.RidReturnedLocked(Rid.Value);
401 if (!
string.IsNullOrEmpty(Echo))
403 Xml.Append(
"' echo='");
407 Xml.Append(
"' xmlns='");
408 Xml.Append(BoshWebClientResource.HttpBindNamespace);
411 string Tx = Xml.ToString();
412 await this.webResource.Return(Response, Tx);
416 private async
void Timeout(
object P)
422 LinkedList<OutputRec> ToCall =
null;
426 if (this.
disposed || this.nextTimeout == DateTime.MinValue)
429 this.nextTimeout = DateTime.MinValue;
433 if (!await this.syncObjOutput.TryBeginWrite(10000))
434 throw new TimeoutException(
"Unable to get access to session.");
438 if (this.waiting is
null)
441 Response = this.waiting;
442 Rid = this.waitingRid;
443 Echo = this.waitingEcho;
445 this.waitingRid =
null;
446 this.waitingEcho =
null;
448 Xml =
new StringBuilder();
450 Xml.Append(
"<body from='");
455 Xml.Append(
"' ack='");
456 Xml.Append(Rid.Value.ToString());
457 this.RidReturnedLocked(Rid.Value);
460 if (!
string.IsNullOrEmpty(Echo))
462 Xml.Append(
"' echo='");
466 Xml.Append(
"' xmlns='");
467 Xml.Append(BoshWebClientResource.HttpBindNamespace);
471 Xml.Append(
"' xmlns:stream='");
472 Xml.Append(XmppClientConnection.StreamNamespace);
475 if (this.outputQueue.First is
null)
481 foreach (OutputRec Fragment
in this.outputQueue)
483 Xml.Append(Fragment);
485 if (!(Fragment.Callback is
null))
487 if (!(ToCall is
null))
488 ToCall =
new LinkedList<OutputRec>();
490 ToCall.AddLast(Fragment);
494 Xml.Append(
"</body>");
496 this.outputQueue.Clear();
501 await this.syncObjOutput.EndWrite();
504 string Tx = Xml.ToString();
505 await this.webResource.Return(Response, Tx);
514 if (!(ToCall is
null))
516 foreach (OutputRec Rec
in ToCall)
533 public override async Task<bool>
BeginWrite(
string Xml, EventHandlerAsync<DeliveryEventArgs> Callback,
object State)
538 if (
string.IsNullOrEmpty(Xml))
543 DateTime Now = DateTime.Now;
549 if (!await this.syncObjOutput.TryBeginWrite(10000))
550 throw new TimeoutException(
"Unable to get access to session.");
554 if (this.waiting is
null)
556 this.outputQueue.AddLast(
new OutputRec()
565 Response = this.waiting;
566 Rid = this.waitingRid;
567 Echo = this.waitingEcho;
569 this.waitingRid =
null;
570 this.waitingEcho =
null;
574 await this.syncObjOutput.EndWrite();
579 StringBuilder Xml2 =
new StringBuilder();
581 Xml2.Append(
"<body from='");
586 Xml2.Append(
"' ack='");
587 Xml2.Append(Rid.Value.ToString());
588 this.RidReturnedLocked(Rid.Value);
591 if (!
string.IsNullOrEmpty(Echo))
593 Xml2.Append(
"' echo='");
597 Xml2.Append(
"' xmlns='");
602 Xml2.Append(
"' xmlns:stream='");
608 Xml2.Append(
"</body>");
610 string Tx = Xml2.ToString();
611 await this.webResource.Return(Response, Tx);
633 public override Task<bool>
StreamError(
string ErrorXml,
string Reason)
635 return this.ToError(
"<stream:error>" + ErrorXml +
"</stream:error>", Reason);
638 private async Task<bool> ToError(
string ErrorXml,
string Reason)
640 this.terminated =
true;
646 if (
string.IsNullOrEmpty(ErrorXml))
653 return await this.
BeginWrite(ErrorXml, async (Sender, e) =>
660 private async Task<bool> ProcessFragment(
string Xml, Stream Stream)
666 if (!
string.IsNullOrEmpty(this.
fullJid))
669 Doc =
XML.
ParseXml(
"<body xmlns='" + BoshWebClientResource.HttpBindNamespace +
"'>" + Xml +
"</body>",
true);
671 return await this.ProcessStanzas(Doc.DocumentElement, Stream);
676 await this.
StreamError(
"<bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>", ex.Message);
682 internal async Task<bool> ProcessStanzasInOrder(
long Rid, XmlElement RootElement, Stream Stream,
string Key)
684 bool Process =
false;
690 throw new TimeoutException(
"Unable to get access to session.");
694 if (!this.lastRid.HasValue || Rid ==
this.lastRid.Value + 1)
696 if (!this.CheckNextKey(
Key))
704 if (this.inputQueue.Count >=
this.hold || (Rid -
this.lastRid.Value) >
this.requests)
706 this.terminated =
true;
707 this.terminationCondition =
"item-not-found";
710 this.inputQueue.AddLast((Rid, RootElement, Stream,
Key));
721 Log.
Error(
"Terminating session. Too many incoming requests.", this.
fullJid);
724 bool FirstElement =
true;
729 FirstElement =
false;
731 XmlElement RootElement0 = RootElement;
732 Stream Stream0 = Stream;
734 LinkedListNode<(long, XmlElement, Stream, string)> First;
735 (long, XmlElement, Stream, string) Rec;
738 throw new TimeoutException(
"Unable to get access to session.");
742 if (!((First = this.inputQueue.First) is
null) &&
743 (Rec = First.Value).Item1 == Rid + 1)
747 RootElement = Rec.Item2;
750 this.inputQueue.RemoveFirst();
752 if (this.CheckNextKey(
Key))
757 while (RootElement is
null &&
758 !((First = this.inputQueue.First) is
null) &&
759 (Rec = First.Value).Item1 == Rid + 1);
761 if (!(RootElement is
null))
774 if (!await this.ProcessStanzas(RootElement0, Stream0))
782 while (!(RootElement is
null));
788 internal bool CheckNextKey(
string Key)
792 if (!(this.key is
null))
798 int i, c = PrevKey.Length;
800 if (c != this.key.Length)
803 for (i = 0; i < c; i++)
805 if (PrevKey[i] != this.key[i])
821 return this.currentStream as SslStream;
824 internal async Task<bool> ProcessStanzas(XmlElement RootElement, Stream Stream)
833 foreach (XmlNode N
in RootElement.ChildNodes)
835 if (N is XmlElement E)
843 this.currentStream = Stream;
850 if (!await this.
StreamError(
"<bad-format xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>", ex.Message))
860 StringBuilder Xml =
new StringBuilder();
861 await this.InitStream(Xml, Stream as SslStream);
862 if (!await this.
BeginWrite(Xml.ToString(),
null,
null))
867 DateTime Now = DateTime.Now;
869 if (this.nrEmpty == 0)
870 this.firstEmpty = Now;
873 if (this.nrEmpty > 5)
875 double SecondsPerRequest = (Now - this.firstEmpty).TotalSeconds / this.nrEmpty;
877 if (SecondsPerRequest * 2 < this.pollingSeconds)
879 this.terminated =
true;
880 this.terminationCondition =
"policy-violation";
881 BoshWebClientResource.Remove(
this);
907 return await this.
StreamError(
"<unsupported-stanza-type xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>",
908 "Unrecognized stanza: " + StanzaElement.LocalName);
911 internal async Task InitStream(StringBuilder sb, SslStream SslStream)
913 sb.Append(
"<stream:features>");
923 if (Mechanism.
Allowed(SslStream))
925 sb.Append(
"<mechanism>");
926 sb.Append(Mechanism.
Name);
927 sb.Append(
"</mechanism>");
931 sb.Append(
"</mechanisms>");
933 if (await this.
server.CanRegister(
this))
934 sb.Append(
"<register xmlns='http://jabber.org/features/iq-register'/>");
940 sb.Append(
"<bind xmlns='" + XmppClientConnection.BindNamespace +
"'/>");
941 sb.Append(
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>");
944 sb.Append(
"</stream:features>");
947 internal void RidReturnedLocked(
long Rid)
949 if (Rid > this.maxRid)
952 this.maxRidTimestamp = DateTime.Now;
962 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 XmlDocument ParseXml(string Xml)
Parses an XML Document from its string representation.
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.
void TransmitText(string Text)
Called when text has been transmitted.
void Exception(Exception Exception)
Called to inform the viewer of an exception state.
void ReceiveText(string Text)
Called when text has been received.
ISniffer[] Sniffers
Registered sniffers.
bool HasSniffers
If there are sniffers registered on the object.
void Error(string Error)
Called to inform the viewer of an error state.
Event arguments for delivery events.
Represets a response of an HTTP client request.
CaseInsensitiveString From
From address
override string RemoteEndPoint
Remote endpoint.
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.
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
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.
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 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, Action< object > 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.