EWS Streaming

Bei der Fehleranalyse eines Skype Room Systems habe ich per Fiddler gesehen, dass hier die EWS Subscription API zum Einsatz kommt. Ich habe die Gelegenheit genutzt, die Traces hier zu dokumentieren. Über die Subscription kann ein Client Änderungen in einem Ordner überwachen und bekommt sofort eine Antwort, wenn sich etwas ändert.

Drei Arten von Streaming

Das Ziel dieser Schnittstellen, ist immer möglichst schnell über Änderungen in einem Postfach informiert zu werden ohne dass man z.B. alle 10 Sekunden wieder die Daten neu einliest. 10 Sekunden sind auch schon eine lange Zeit aber vor allem muss man so immer wieder alles lesen. Benachrichtigungen von Exchange sind hier eine Lösung. Davon gibt es sogar drei

  • Push per WebService
    Sie können einen WebServer aufsetzen, der per HTTPS erreichbar ist und diesen bei Exchange hinterlegen. Sobald sich dann etwas im Postfach ändert, ruft Exchange ihren WebService auf und sie können aktiv werden. Diese Option wird aber eher selten genutzt, denn Sie müssen ja den WebService betreiben und mit Office 365 müsste er auch mit einem offiziellen Namen und Zertifikat zumindest von den Office 365 IP-Adressen erreichbar sein. Das ist zwar eine sehr enge Kopplung aber nicht einfach
  • Streaming
    Über diese Option, die ich weiter unten auch beschreibe, kann ein Client einen HTTP-Request stellen und Exchange "streamt" quasi Benachrichtigungen über Änderungen an den Prozess zurück. Über einen Request können also gleich mehrere Rückmeldungen kommen.
  • Pull Benachrichtigungen
    Diese Funktion ist etwas einfacher, das der Client einen Request sendet, der mit einer Änderung auch beantwortet und abgeschlossen wird. Der Client muss dann natürlich wieder einen Request erstellen, wenn er weiter auf dem Laufenden bleiben will.

Eine sehr lesenswerte Seite über die drei verschiedenen Optionen, auf Veränderungen in einer Mailbox zu reagiere, hat Microsoft hier veröffentlicht.

Neben diesen drei Optionen gibt es natürlich noch zwei andere Wege ein Postfach auf Änderungen zu überwachen.

  • Einfaches Polling
    Niemand kann ihnen verbieten, einfach selbst immer wieder nach Änderungen zu suchen, indem Sie z.B. nach "Last modified Date" suchen. Das ist ein aber ineffektives "Polling" da sie viel mehr Requests und Daten übertragen. Zudem ist ein Mehraufwand erforderlich, um z.B. "Rename" und "Delete"-Operationen korrekt zu verarbeiten.
  • Sync-API
    Eine etwas elegantere Version für ein Polling ist die Sync API mit der Exchange nach dem Lesen der Daten einen Cookie mit ausliefert. Der Client kann beim nächsten "Sync" dann diesen Cookie mitsenden und Exchange liefert dann direkt die "Änderungen" und nur die Änderungen.

Fiddler Übersicht

In der Sicht der Zugriff sehen Sie hier so eine Verbindung eines Clients (hier ein Skype Room System zur Abfrage der Meetings in einem Raumpostfach-Kalender)

Der erste Zugriff im Paket 223 ist noch ein anonymer Zugriff, da der Client ja noch gar nicht weiß, welche Authentifizierung der Server anbietet. Die sonstigen "Vorarbeiten" wie z.B. Autodiscover-Anfragen, ADFS-Ticket-Anforderung etc. habe ich hier nicht weiter aufgeführt und sind nicht für die Subscription-Funktion abweichend.

Die Pakete 224-751 zeigen in der Vergangenheit erfolgte Abfragen und ihre Antwort. Es ist gut zu sehen, dass hier der Client ca. alle 2 Minuten die Subscription erneuert. Ich konnte noch nicht feststellen, ob das nun einem TCP-Timeout, dem Proxy oder einer Firewall geschuldet ist, die so eine "inaktive Sitzung" unterbricht.

Das letzte Paket zeigt aber, dass die Anfrage gestellt aber die Länge des Body noch 0 ist und es auch keinen "Antwortcode" (Result) gibt. Normalerweise zeigt Fiddler dies an, wenn ein HTTP-Request gestellt aber die Antwort noch nicht angekommen ist. Bei normalen Request sehen Sie diesen Zustand also nur, wenn die Gegenseite nicht erreichbar oder sehr langsam wäre. Normalerweise versuchen WebServer eine Anfrage ja möglichst schnell zu beantworten.

Hier ist es aber so, dass der Client ja eine Anfrage stellt und gerade darauf hofft, dass der Server seine Antwort zurück hält, bis es etwas zu vermelden gibt. Schauen wir uns daher mal die Pakete etwas genauer an:

Subscription Request 224

Das ist die erste Anforderung nachdem der Client ermittelt hat, dass dieser Service per Basic-Authentication erreichbar ist. Sie können ohne viel Fachwissen schon sehen, dass der Client den Ordner "Calendar" überwachen will und hierzu sowohl "CreatedEvent" als auch "DeletedEvent", "ModifiedEvent", "MovedEvent", "CopiedEvent" und "FreeBusyChangedEvent" anfordert.

POST https://outlook.office365.com/EWS/Exchange.asmx HTTP/1.1
Connection: keep-alive
Accept: text/xml User-Agent: SkypeRoomSystem/3.1.112.0
Content-Length: 1027
Content-Type: text/xml; charset=utf-8
Host: outlook.office365.com
Authorization: Basic xxxxxxxxxxxxx=

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013_SP1" />
  </soap:Header>
  <soap:Body>
    <m:Subscribe>
      <m:StreamingSubscriptionRequest>
        <t:FolderIds>
          <t:DistinguishedFolderId Id="calendar" />
        </t:FolderIds>
        <t:EventTypes>
          <t:EventType>CreatedEvent</t:EventType>
          <t:EventType>DeletedEvent</t:EventType>
          <t:EventType>ModifiedEvent</t:EventType>
          <t:EventType>MovedEvent</t:EventType>
          <t:EventType>CopiedEvent</t:EventType>
          <t:EventType>FreeBusyChangedEvent</t:EventType>
        </t:EventTypes>
      </m:StreamingSubscriptionRequest>
    </m:Subscribe>
  </soap:Body>
</soap:Envelope>

Subscription Antwort 224

Der Server antwortet dem Client einfach nur mit einer "SubscriptionId", unter der der Server die Anfrage weiterführt. Beachten Sie, dass die Antwort ein "Content-Length"-Property hat. Dieser Request ist damit schon abgeschlossen.

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/xml; charset=utf-8
Server: Microsoft-IIS/10.0
request-id: 9bd9f62d-8ee9-4b46-acab-e65cd2dbf923
X-CalculatedFETarget: VI1PR10CU003.internal.outlook.com
X-BackEndHttpStatus: 200
Set-Cookie: exchangecookie=81a41de6f7804c78a47c188a874bb380; expires=Fri, 07-Jun-2019 13:01:31 GMT; path=/; HttpOnly
X-FEProxyInfo: VI1PR10CA0094.EURPRD10.PROD.OUTLOOK.COM
X-CalculatedBETarget: VI1PR0402MB3646.eurprd04.prod.outlook.com
X-BackEndHttpStatus: 200
X-RUM-Validated: 1
x-EwsHandler: Subscribe
X-AspNet-Version: 4.0.30319
X-BeSku: WCS5
X-DiagInfo: VI1PR0402MB3646
X-BEServer: VI1PR0402MB3646
X-FEServer: VI1PR10CA0094
X-Powered-By: ASP.NET
X-FEServer: AM5PR0701CA0001
Date: Thu, 07 Jun 2018 13:01:31 GMT
Content-Length: 1049

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
  <h:ServerVersionInfo MajorVersion="15" MinorVersion="20" MajorBuildNumber="820" MinorBuildNumber="16" Version="V2018_01_08" 
     xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" 
     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
  </s:Header>
  <s:Body>
    <m:SubscribeResponse 
      xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
      xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
      <m:ResponseMessages>
        <m:SubscribeResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:SubscriptionId>KQB2aTFwcjA0MDJtYjM2ND...==</m:SubscriptionId>
        </m:SubscribeResponseMessage>
      </m:ResponseMessages>
    </m:SubscribeResponse>
  </s:Body>
</s:Envelope>

Streaming Request 225

Der Client stellt direkt darauf eine Anfrage mit der SubscriptionID als Parameter und einen Timeout, wie lange die Anfrage "offen" bleiben darf

POST https://outlook.office365.com/EWS/Exchange.asmx HTTP/1.1
Connection: keep-alive
Cookie: exchangecookie=81a41de6f7804c78a47c188a874bb380
Accept: text/xml User-Agent: SkypeRoomSystem/3.1.112.0
Content-Length: 782
Content-Type: text/xml; charset=utf-8
Host: outlook.office365.com
Authorization: Basic xxxxxxxx=

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" 
    xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013_SP1" />
  </soap:Header>
  <soap:Body>
    <m:GetStreamingEvents>
      <m:SubscriptionIds>
        <t:SubscriptionId>KQB2aTFwcjA0MDJtYjM2ND...==</t:SubscriptionId>
      </m:SubscriptionIds>
      <m:ConnectionTimeout>15</m:ConnectionTimeout>
    </m:GetStreamingEvents>
  </soap:Body>
</soap:Envelope>

Hier wird nicht mehr auf den Ordner oder die Events referenziert. Das weiß Exchange ja schon durch das Ausstellen der SubscriptionID

Antwort 226

Der Exchange Server beantwortet diese Frage sehr schnell aber terminiert das Paket nicht.

HTTP/1.1 200 OK
Cache-Control: private
Server: Microsoft-IIS/10.0
request-id: b194d283-8405-4110-bdd9-1cba3a273061
X-CalculatedFETarget: VI1PR10CU003.internal.outlook.com
X-BackEndHttpStatus: 200
Set-Cookie: exchangecookie=xxxxxx; path=/
X-FEProxyInfo: VI1PR10CA0115.EURPRD10.PROD.OUTLOOK.COM
X-CalculatedBETarget: VI1PR0402MB3646.eurprd04.prod.outlook.com
X-BackEndHttpStatus: 200
X-RUM-Validated: 1
X-NoBuffering: 1
X-AspNet-Version: 4.0.30319
X-BeSku: WCS5
X-DiagInfo: VI1PR0402MB3646
X-BEServer: VI1PR0402MB3646
X-FEServer: VI1PR10CA0115
X-Powered-By: ASP.NET
X-FEServer: AM5PR0701CA0001
Date: Thu, 07 Jun 2018 13:01:31 GMT
Content-Length: 28312

<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">......</Envelope>

Interessant ist, dass der Content nun nicht aus einer XML-Datei mit einem Root-Knoten besteht, sondern aus vielen identischen Zeilen. Dieser werden aber wohl nicht auf einmal gesendet sondern immer wieder wiederholt, bis die TCP-Verbindung abbricht. Bei mir war diese Antwort 24mal vorhanden. Der Webserver sendet also immer mal wieder eine "Zwischenantwort", damit der Client weiss, dass die Verbindung noch besteht.

Antwort 787

Hier sehen Sie die Anzeige in Fiddler, wenn ein Request noch nicht abgeschlossen ist. Die angezeigte Content-Length ist 0 und auch der Statuscode ist noch nicht gesetzt". Die Antwort ist zumindest auch Fiddler auch noch nicht mit einem "Content-Length" gefüllt.

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/xml; charset=utf-8
Server: Microsoft-IIS/10.0
request-id: 96f2392f-eb4f-4a90-8617-61a918478fae
X-CalculatedFETarget: VI1PR10CU003.internal.outlook.com
X-BackEndHttpStatus: 200
Set-Cookie: exchangecookie=5cd22223bdcc49daa9f5cd15de2fcfca; expires=Fri, 07-Jun-2019 13:08:01 GMT; path=/; HttpOnly
X-FEProxyInfo: VI1PR10CA0105.EURPRD10.PROD.OUTLOOK.COM
X-CalculatedBETarget: VI1PR0402MB3646.eurprd04.prod.outlook.com
X-BackEndHttpStatus: 200
X-RUM-Validated: 1
x-EwsHandler: Subscribe
X-AspNet-Version: 4.0.30319
X-BeSku: WCS5
X-DiagInfo: VI1PR0402MB3646
X-BEServer: VI1PR0402MB3646
X-FEServer: VI1PR10CA0105
X-Powered-By: ASP.NET
X-FEServer: AM6PR0402CA0034
Date: Thu, 07 Jun 2018 13:08:01 GMT

Das dürfte aber eher an Fiddler liegen.

Meldungen im Eventlog

Als gute Quelle bei der Fehlersucht hat sich auch das Eventlog des Exchange Servers gezeigt. Das funktioniert natürölich nur mit lokalen eigenen Exchange Servern. Auf das Eventlog von Exchange Servern in Office 365 haben sie natürlich keinen Zugriff. Hier eine Beispielmeldung

Log Name:      Application
Source:        MSExchange Web Services
Event ID:      7
Task Category: Core
Level:         Error
Keywords:      Classic User:          N/A
Computer:      EX2016.msxfaq.net
Description:
After 8 unsuccessful attempts to send a notification for subscription [Gg...  +yRL22KJbg==] against endpoint 
   [https://sfbweb.msxfaq.net /Storage/Notification.ashx?server=SfBFE01.msxfaq.net&cwt=AAM... Wdl], 
   the subscription has been removed. 
Details: WebException: Unable to connect to the remote server 
Status: ConnectFailure    at System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult, TransportContext& context)
   at System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult)
   at Microsoft.Exchange.Services.Core.NotificationServiceClient.CreateSendNotificationRequestAsync(IAsyncResult requestAsyncResult)

Hier hat wohl ein Client eine EWS-Rückruffunktion hinterlegt, die der Server aber nicht mehr erreichen konnte. Das ist erklärbar, wenn der eigene Webservice nicht mehr online ist. Es kann aber auch daran liegen, dass Exchange den Service einfach nicht erreicht. Sei es wegen dynamischen IP-Adressen, Blockaden durch Firewalls und Proxy-Server oder der Entwickler eine falsche URL hinterlegt hat.

Fiddler als Störer

Wer misst, kann durch die Messung natürlich auch die Ergebnisse verfälschen. Hier scheint das auch so zu sein, als ich zur Fehlersuche mit Fiddler mich dazwischen geklemmt habe um die SSL-Verbindung zu analysieren. Die Anzeige von Fiddler ist korrekt aber der Client hat anscheinend nicht immer die Kalenderdaten zuverlässig bekommen. Erst als ich den Proxy entfernt habe, hat der Abgleich geklappt.

Die Ursache habe ich nicht genauer ermitteln können, da die Abfragen alle per HTTPS verschlüsselt waren.

Wenn ich mal dazu komme, versuche ich per PowerShell die StreamingAPI ohne Verschlüsselung zu nutzen. Dann sollte auch Wireshark genauer ermitteln können, was hier über die Leitung geht.

PS Sample für Inbox

Um die Funktion zu zeigen, habe ich ein PowerShell-Sample, welches ohne Fehlerbehandlung eine EWS-Verbindung zur Inbox aufbaut und auf neue Mails wartet. Wenn Sie dieses Skript gegen einen On-Premise Exchange Server ohne HTTPS-Zwang einsetzen, dann können Sie auch mit Wireshark oder NetMon die übertragenen Daten einfach analysiere.

Aktuell habe ich noch kein Sample zur Veröffentlichung bereit.

Solche Aufgabenstellung sind auch eher in kommerziellen Produkten, z.B. Migrationen, Neartime-Verarbeitung o.ä., zu sehen und werden in PowerShell eher selten genutzt. Hier machen dann eher die "Pull Subscription" oder ein einfaches Polling mehr Sinn.

Weitere Links