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