Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
BoshWebClientResource.cs
1using System;
2using System.Net.Security;
3using System.Text;
4using System.Threading.Tasks;
5using System.Xml;
6using Waher.Content;
8using Waher.Events;
13
15{
20 {
21 private const int MaxSecondsIdle = 90;
22
23 private readonly static Cache<string, BoshSession> sessions = GetCache();
24
25 private readonly XmppServer xmppServer;
26 private readonly HttpServer httpServer;
27
35 : base(ResourceName)
36 {
37 this.xmppServer = XmppServer;
38 this.httpServer = HttpServer;
39 }
40
41 private static Cache<string, BoshSession> GetCache()
42 {
43 Cache<string, BoshSession> Result = new Cache<string, BoshSession>(int.MaxValue, TimeSpan.MaxValue, TimeSpan.FromSeconds(MaxSecondsIdle), true);
44
45 Result.Removed += Result_Removed;
46
47 return Result;
48 }
49
50 private static async Task Result_Removed(object Sender, CacheItemEventArgs<string, BoshSession> e)
51 {
52 try
53 {
54 e.Value.Removed = true;
55 await e.Value.DisposeAsync();
56 }
57 catch (Exception ex)
58 {
59 Log.Exception(ex);
60 }
61 }
62
66 public const string HttpBindNamespace = "http://jabber.org/protocol/httpbind";
67
71 public bool AllowsPOST => true;
72
76 public bool AllowsGET => true;
77
81 public override bool HandlesSubPaths => true;
82
86 public override bool UserSessions => false;
87
93 public Task GET(HttpRequest Request, HttpResponse Response)
94 {
95 string Xml = System.Web.HttpUtility.UrlDecode(Request.Header.QueryString);
96 if (string.IsNullOrEmpty(Xml))
97 throw new BadRequestException();
98
99 Response.SetHeader("Cache-Control", "max-age=0, no-cache, no-store");
100
101 this.Process(Xml, Request, Response);
102
103 return Task.CompletedTask;
104 }
105
111 public Task POST(HttpRequest Request, HttpResponse Response)
112 {
113 if (!Request.HasData)
114 throw new BadRequestException();
115
116 // Content type in request header cannot be trusted. Content should be assumed to be UTF-8 encoded XML.
117
118 long l = Request.DataStream.Length;
119 if (l > int.MaxValue)
120 throw new BadRequestException();
121
122 int Len = (int)l;
123 byte[] Data = new byte[Len];
124 Request.DataStream.Position = 0;
125 Request.DataStream.ReadAll(Data, 0, Len);
126
127 string Xml = Encoding.UTF8.GetString(Data);
128 this.Process(Xml, Request, Response);
129
130 return Task.CompletedTask;
131 }
132
133 private async void Process(string Xml, HttpRequest Request, HttpResponse Response)
134 {
135 try
136 {
137 await this.ProcessFragment(Xml, Request, Response);
138 }
139 catch (Exception ex)
140 {
141 Log.Exception(ex);
142 }
143 }
144
145 private async Task<bool> ProcessFragment(string Xml, HttpRequest Request, HttpResponse Response)
146 {
147 string Name;
148 int i, c;
149 int State = 5;
150 int Depth = 0;
151 int Delta;
152 int Start = 0;
153 char ch;
154 bool HasNamespace = false;
155
156 c = Xml.Length;
157 for (i = 0; i < c; i++)
158 {
159 ch = Xml[i];
160
161 switch (State)
162 {
163 case 5: // Waiting for start element.
164 if (ch == '<')
165 State++;
166 break;
167
168 case 6: // Second character in tag
169 if (ch == '/')
170 State++;
171 else if (ch == '!')
172 State = 13;
173 else
174 State += 2;
175 break;
176
177 case 7: // Waiting for end of closing tag
178 if (ch == '>')
179 {
180 Depth--;
181 if (Depth < 0)
182 {
183 await this.BoshError(Response, "Invalid Request");
184 return false;
185 }
186 else
187 State = 5;
188 }
189 break;
190
191 case 8: // Wait for end of start tag
192 if (ch == '>' || ch == '/')
193 {
194 if (Depth == 1 && !HasNamespace)
195 {
196 Name = " xmlns='" + XmppClientConnection.C2SNamespace + "'";
197 Xml = Xml.Insert(i, Name);
198 Delta = Name.Length;
199 i += Delta;
200 c += Delta;
201 }
202
203 if (ch == '>')
204 {
205 Depth++;
206 State = 5;
207 }
208 else
209 State++;
210 }
211 else if (ch <= ' ')
212 {
213 State += 2;
214 Start = i + 1;
215 HasNamespace = false;
216 }
217 break;
218
219 case 9: // Check for end of childless tag.
220 if (ch == '>')
221 State = 5;
222 else
223 State--;
224 break;
225
226 case 10: // Check for attributes.
227 if (ch == '>' || ch == '/')
228 {
229 if (Depth == 1 && !HasNamespace)
230 {
231 Name = " xmlns='" + XmppClientConnection.C2SNamespace + "'";
232 Xml = Xml.Insert(i, Name);
233 Delta = Name.Length;
234 i += Delta;
235 c += Delta;
236 }
237
238 if (ch == '>')
239 {
240 Depth++;
241 State = 5;
242 }
243 else
244 State--;
245 }
246 else if (ch == '"')
247 State++;
248 else if (ch == '\'')
249 State += 2;
250 else if (ch == '=')
251 {
252 if (Depth == 1)
253 {
254 Name = Xml.Substring(Start, i - Start);
255 if (Name == "xmlns")
256 HasNamespace = true;
257 }
258 }
259 else if (ch <= ' ')
260 Start = i + 1;
261 break;
262
263 case 11: // Double quote attribute.
264 if (ch == '"')
265 State--;
266 break;
267
268 case 12: // Single quote attribute.
269 if (ch == '\'')
270 State -= 2;
271 break;
272
273 case 13: // Third character in start of comment
274 if (ch == '-')
275 State++;
276 else if (ch == '[')
277 State = 18;
278 else
279 {
280 await this.BoshError(Response, "Invalid Request");
281 return false;
282 }
283 break;
284
285 case 14: // Fourth character in start of comment
286 if (ch == '-')
287 State++;
288 else
289 {
290 await this.BoshError(Response, "Invalid Request");
291 return false;
292 }
293 break;
294
295 case 15: // In comment
296 if (ch == '-')
297 State++;
298 break;
299
300 case 16: // Second character in end of comment
301 if (ch == '-')
302 State++;
303 else
304 State--;
305 break;
306
307 case 17: // Third character in end of comment
308 if (ch == '>')
309 State = 5;
310 else
311 State -= 2;
312 break;
313
314 case 18: // Fourth character in start of CDATA
315 if (ch == 'C')
316 State++;
317 else
318 {
319 await this.BoshError(Response, "Invalid Request");
320 return false;
321 }
322 break;
323
324 case 19: // Fifth character in start of CDATA
325 if (ch == 'D')
326 State++;
327 else
328 {
329 await this.BoshError(Response, "Invalid Request");
330 return false;
331 }
332 break;
333
334 case 20: // Sixth character in start of CDATA
335 if (ch == 'A')
336 State++;
337 else
338 {
339 await this.BoshError(Response, "Invalid Request");
340 return false;
341 }
342 break;
343
344 case 21: // Seventh character in start of CDATA
345 if (ch == 'T')
346 State++;
347 else
348 {
349 await this.BoshError(Response, "Invalid Request");
350 return false;
351 }
352 break;
353
354 case 22: // Eighth character in start of CDATA
355 if (ch == 'A')
356 State++;
357 else
358 {
359 await this.BoshError(Response, "Invalid Request");
360 return false;
361 }
362 break;
363
364 case 23: // Ninth character in start of CDATA
365 if (ch == '[')
366 State++;
367 else
368 {
369 await this.BoshError(Response, "Invalid Request");
370 return false;
371 }
372 break;
373
374 case 24: // In CDATA
375 if (ch == ']')
376 State++;
377 break;
378
379 case 25: // Second character in end of CDATA
380 if (ch == ']')
381 State++;
382 else
383 State--;
384 break;
385
386 case 26: // Third character in end of CDATA
387 if (ch == '>')
388 State = 5;
389 else if (ch != ']')
390 State -= 2;
391 break;
392
393 default:
394 break;
395 }
396 }
397
398 XmlDocument Doc = new XmlDocument()
399 {
400 PreserveWhitespace = true
401 };
402 XmlElement Body;
403
404 try
405 {
406 Doc.LoadXml(Xml);
407 }
408 catch (Exception)
409 {
410 throw new BadRequestException();
411 }
412
413 Body = Doc.DocumentElement;
414 if (Body is null || Body.LocalName != "body" || Body.NamespaceURI != HttpBindNamespace)
415 throw new BadRequestException();
416
417 string ContentType = "text/xml; charset=utf-8";
418 string Language = "en";
419 CaseInsensitiveString From = null;
420 CaseInsensitiveString To = null;
421 string Sid = null;
422 string Key = null;
423 string NewKey = null;
424 string Echo = null;
425 double Version = double.MaxValue;
426 long? Rid = null;
427 long? Ack = null;
428 int Hold = 1;
429 int WaitSeconds = 30;
430 bool Secure = false;
431 bool Terminate = false;
432
433 foreach (XmlAttribute Attribute in Body.Attributes)
434 {
435 switch (Attribute.Name)
436 {
437 case "content":
438 ContentType = Attribute.Value;
439 break;
440
441 case "from":
442 From = Attribute.Value;
443 break;
444
445 case "hold":
446 if (!int.TryParse(Attribute.Value, out Hold) || Hold < 0)
447 throw new BadRequestException();
448
449 if (Hold > 5)
450 Hold = 5;
451 break;
452
453 case "rid":
454 if (!long.TryParse(Attribute.Value, out long l) || l < 0)
455 throw new BadRequestException();
456 Rid = l;
457 break;
458
459 case "sid":
460 Sid = Attribute.Value;
461 break;
462
463 case "to":
464 To = Attribute.Value;
465 break;
466
467 case "route":
468 // Ignore.
469 break;
470
471 case "ver":
472 if (!CommonTypes.TryParse(Attribute.Value, out Version) || Version < 1.0)
473 throw new BadRequestException();
474 break;
475
476 case "wait":
477 if (!int.TryParse(Attribute.Value, out WaitSeconds) || Hold <= 0)
478 throw new BadRequestException();
479 break;
480
481 case "ack":
482 if (!long.TryParse(Attribute.Value, out l))
483 throw new BadRequestException();
484 else
485 Ack = l;
486 break;
487
488 case "secure":
489 if (!CommonTypes.TryParse(Attribute.Value, out Secure))
490 throw new BadRequestException();
491 break;
492
493 case "xml:lang":
494 Language = Attribute.Value;
495 break;
496
497 case "type":
498 if (Attribute.Value == "terminate")
499 Terminate = true;
500 break;
501
502 case "key":
503 Key = Attribute.Value;
504 break;
505
506 case "newkey":
507 NewKey = Attribute.Value;
508 break;
509
510 case "echo":
511 Echo = Attribute.Value;
512 break;
513
514 default:
515 break;
516 }
517 }
518
519 Response.ContentType = ContentType;
520
521 if (Version > 1.11)
522 Version = 1.11;
523
524 StringBuilder sb = new StringBuilder();
525 BoshSession Session;
526
527 if (string.IsNullOrEmpty(Sid))
528 {
529 if (string.IsNullOrEmpty(To) || !this.xmppServer.IsServerDomain(To, true) || !(Key is null))
530 throw new BadRequestException();
531
532 // TODO: Check against spam. Restrict number of active sessions allowed per remote IP
533
534 do
535 {
536 Sid = this.xmppServer.GetRandomHexString(16);
537 }
538 while (sessions.ContainsKey(Sid));
539
540 if (WaitSeconds > MaxSecondsIdle)
541 WaitSeconds = MaxSecondsIdle;
542
543 int PollingSeconds = Math.Min(5, WaitSeconds);
544 int Requests = Hold + 1;
545
546 Session = new BoshSession(this, Sid, Rid, Version, From, To, Language, Hold, WaitSeconds, PollingSeconds, Requests,
547 Ack.HasValue && Ack.Value != 0, Secure, StripPort(Request.RemoteEndPoint), NewKey, this.xmppServer);
548 sessions.Add(Sid, Session);
549
550 sb.Append("<body wait='");
551 sb.Append(WaitSeconds.ToString());
552 sb.Append("' inactivity='");
553 sb.Append(Math.Max(WaitSeconds / 2, 1).ToString());
554 sb.Append("' polling='");
555 sb.Append(PollingSeconds.ToString());
556 sb.Append("' requests='");
557 sb.Append(Requests.ToString());
558 sb.Append("' hold='");
559 sb.Append(Hold.ToString());
560
561 if (Ack.HasValue && Ack.Value != 0 && Rid.HasValue)
562 {
563 sb.Append("' ack='");
564 sb.Append(Rid.Value.ToString());
565 Session.RidReturnedLocked(Rid.Value);
566 }
567
568 // TODO: 'accept' attribute for content encodings.
569
570 sb.Append("' sid='");
571 sb.Append(XML.Encode(Sid));
572 sb.Append("' ver='");
573 sb.Append(CommonTypes.Encode(Version));
574 sb.Append("' from='");
575 sb.Append(XML.Encode(To));
576
577 if (!string.IsNullOrEmpty(Echo))
578 {
579 sb.Append("' echo='");
580 sb.Append(XML.Encode(Echo));
581 }
582
583 sb.Append("' xmlns='");
584 sb.Append(HttpBindNamespace);
585 sb.Append("' xmlns:stream='");
586 sb.Append(XmppClientConnection.StreamNamespace);
587 sb.Append("'>");
588
589 await Session.InitStream(sb, Response.ClientConnection.Stream as SslStream);
590
591 sb.Append("</body>");
592
593 await this.Return(Response, sb.ToString());
594 }
595 else
596 {
597 if (!sessions.TryGetValue(Sid, out Session))
598 throw new NotFoundException();
599
600 if (Session.RemoteEndpoint != StripPort(Request.RemoteEndPoint))
601 throw new ForbiddenException("Invalid remote endpoint (BOSH)");
602
603 if (Session.State == XmppConnectionState.Active && !this.xmppServer.TouchClientConnection(Session.FullJid))
604 throw new NotFoundException();
605
606 bool KeyOk;
607
608 if (Rid.HasValue)
609 KeyOk = await Session.ProcessStanzasInOrder(Rid.Value, Body, Response.ClientConnection.Stream, Key);
610 else
611 {
612 KeyOk = Session.CheckNextKey(Key);
613 if (KeyOk)
614 await Session.ProcessStanzas(Body, Response.ClientConnection.Stream);
615 }
616
617 if (!KeyOk)
618 {
619 await this.BoshError(Response, "item-not-found");
620 return false;
621 }
622
623 if (!(NewKey is null))
624 Session.Key = NewKey;
625
626 if (Terminate)
627 {
628 Session.Terminated = true;
629
630 foreach (XmlNode N in Body.ChildNodes)
631 {
632 if (N is XmlElement E && E.LocalName == "presence" && XML.Attribute(E, "type") == "unavailable")
633 Session.IsBound = false;
634 }
635
636 await Session.DisposeAsync();
637 return false;
638 }
639
640 if (Session.Terminated)
641 await Session.DisposeAsync();
642 else
643 await Session.RegisterForResponse(XmppServer.Scheduler, Rid, Response, Echo);
644 }
645
646 return true;
647 }
648
649 internal Task BoshError(HttpResponse Response, string Condition)
650 {
651 StringBuilder Xml = new StringBuilder();
652
653 Xml.Append("<body type='terminate");
654
655 if (!string.IsNullOrEmpty(Condition))
656 {
657 Xml.Append("' condition='");
658 Xml.Append(Condition);
659 }
660
661 Xml.Append("' xmlns='");
662 Xml.Append(HttpBindNamespace);
663 Xml.Append("'/>");
664
665 return this.Return(Response, Xml.ToString());
666 }
667
668 internal static void Remove(BoshSession Session)
669 {
670 sessions.Remove(Session.Sid);
671 }
672
673 internal async Task Return(HttpResponse Response, string Xml)
674 {
675 byte[] Bin = Encoding.UTF8.GetBytes(Xml);
676
677 Response.ContentLength = Bin.Length; // Avoid chunked service. (XEP-0124, §5).
678
679 try
680 {
681 await Response.Write(Bin);
682 await Response.SendResponse();
683 }
684 catch (Exception ex)
685 {
686 Log.Error(ex.Message, this.ResourceName, Response.Request.RemoteEndPoint);
687 this.httpServer.RequestResponded(Response.Request, 500);
688 }
689 }
690
691 private static string StripPort(string s)
692 {
693 int i = s.LastIndexOf(':');
694 if (i < 0)
695 return s;
696 else
697 return s.Substring(0, i);
698 }
699
700 }
701}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:13
static string Encode(bool x)
Encodes a Boolean for use in XML and other formats.
Definition: CommonTypes.cs:594
static bool TryParse(string s, out double Value)
Tries to decode a string encoded double.
Definition: CommonTypes.cs:46
Helps with common XML-related tasks.
Definition: XML.cs:19
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Definition: XML.cs:914
static string Encode(string s)
Encodes a string for use in XML.
Definition: XML.cs:27
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:13
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.
Definition: Log.cs:1647
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.
Definition: Log.cs:682
Stream Stream
Stream object currently being used.
The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repe...
The server understood the request, but is refusing to fulfill it. Authorization will not help and the...
Base class for all asynchronous HTTP resources. An asynchronous resource responds outside of the meth...
string QueryString
Query string. To get the values of individual query parameters, use the TryGetQueryParameter method.
Represents an HTTP request.
Definition: HttpRequest.cs:18
Stream DataStream
Data stream, if data is available, or null if data is not available.
Definition: HttpRequest.cs:139
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:134
string RemoteEndPoint
Remote end-point.
Definition: HttpRequest.cs:195
bool HasData
If the request has data.
Definition: HttpRequest.cs:74
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:21
async Task SendResponse()
Sends the response back to the client. If the resource is synchronous, there's no need to call this m...
HttpRequest Request
Corresponding HTTP Request
void SetHeader(string FieldName, string Value)
Sets a custom header field value.
BinaryTcpClient ClientConnection
Current client connection
async Task Write(byte[] Data)
Returns binary data in the response.
Implements an HTTP server.
Definition: HttpServer.cs:36
The server has not found anything matching the Request-URI. No indication is given of whether the con...
Task GET(HttpRequest Request, HttpResponse Response)
Handles a GET requesst.
BoshWebClientResource(XmppServer XmppServer, HttpServer HttpServer, string ResourceName)
BOSH webclient interface
Task POST(HttpRequest Request, HttpResponse Response)
Handles a POST requesst.
override bool UserSessions
If User session is requried
const string HttpBindNamespace
http://jabber.org/protocol/httpbind
Represents a case-insensitive string.
string Value
String-representation of the case-insensitive string. (Representation is case sensitive....
Implements an in-memory cache.
Definition: Cache.cs:15
Event arguments for cache item removal events.
ValueType Value
Value of item that was removed.
GET Interface for HTTP resources.
POST Interface for HTTP resources.
XmppConnectionState
State of XMPP connection.
ContentType
DTLS Record content type.
Definition: Enumerations.cs:11