6.0.0-git
2024-04-25

Diff for ActiveSync/Development between 4 and 5

[[toc]]

+ ActiveSync Library Technical Information

This page is designed to give anyone working on the ActiveSync library in Horde 5.x a general overview of logic flow and what happens where and when.

++ General Library Structure

The logic for handling ActiveSync requests is split between 3 different libraries currently - //Horde_Rpc//, //Horde_Core//, and obviously //Horde_ActiveSync//. Horde_Rpc only handles the initial request and basically just passes on control to Horde_ActiveSync so this page will focus only on the other two libraries.

+++ Horde_ActiveSync

This library contains the main logic for decoding WBXML from EAS requests, passing the request to the appropriate controller and then sending properly encoded responses back to the client.

++++ TODO general description of the classes

+++ Horde_Core

Core contains any code specific to handling Horde Groupware collections. This is where requests for information and changes to information are actually handled.

++++ Factories

: [[# activesyncserver Horde_Core_Factory_ActiveSyncServer]] : Creates the main //Horde_ActiveSync// object. Injects the //Horde_Core_ActiveSync_Driver//, the WBXML encoder/decoder objects, the //Horde_ActiveSync_State_[Sql|Mongo]// storage object and the //Horde_Controller_Request// object.

: Horde_Core_Factory_ActiveSyncState : Creates the state storage handler. The name is misleading (and will change in Horde 6) as this class has evolved to be more of a general storage handler and now handles more than just device state. We will assume a Sql storage backend for this document.

: Horde_Core_Factory_ActiveSyncBackend : Creates the //Horde_Core_ActiveSync_Driver// backend driver. Injects //Horde_Core_ActiveSync_Connector//, //Horde_ActiveSync_Imap_Adapter// (if needed), the state storage driver, and the //Horde_Core_ActiveSync_Auth// object.

++++ TODO ....

++ Protocol Overview

It's beyond the scope to explain the ActiveSync protocol in detail. For that, there is the official [https://msdn.microsoft.com/en-us/library/cc425499(v=exchg.80).aspx documentation]. The most useful of these are
: MS-ASCMD : Details every command/request and it's schema.
: MS-ASHTTP : Documents the requirements and flow of the HTTP protocol as used in the EAS protocol.

There are also the individual documents for each collection type, such as //MS-ASEMAIL//, //MS-ASTASKS// etc... These documents are the best place to start when trying to track down issues such as "Protocol Error" issues with certain clients.

The basic bit to know about the EAS protocol is that it is encoded using WBXML. That is, binary encoded XML data. Again, the structure/schema of WBXML data is beyond the scope of this page. Instead of decoding the entire request first and then handling it, Horde_ActiveSync decodes the data and handles it on the fly. That is, we decode each individual message/object as it comes in and handle it in-line, so to speak. This is to avoid keeping more in memory than is necessary. The same is true for encoding - we don't wait to enocde the entire response - we encode and send the response to the output stream as soon as we can. The actual encoding/decoding is done in the //Horde_ActiveSync_Wbxml_*// classes. The codepages/schema is defined in //Horde_ActiveSync_Wbxml::.

++ Logic Flow

I have a long-standing item on my todo list to generate an activity diagram for the program flow of an EAS request, but in the meantime here is a description of what happens.

EAS requests hit rpc.php first. There are a number of ways that these are differentiated from other RPC requests, but the main give-away is the REQUEST_URI containing //Microsoft-Server-ActiveSync//. A few things happen here before continuing. First, EAS requests are session-less, meaning that the entire Horde environment needs to be setup for each request. This is handled by explicitly setting $session_control = 'None' to force the use of the Null session driver in Horde. From there, we perform the same logic and checks like every other Horde RPC request. This includes having to initialize the Horde environment with NO authentication.

From here, we instantiate a //Horde_Rpc_ActiveSync// object and inject a //Horde_ActiveSync// object (which is created using //Horde_Core_Factory_ActiveSyncServer//).

Here, we perform some sanity checking on the request and sniff out what type of request we are handling. OPTIONS and Autodiscover requests are handled a little differently, but for now we will concentrate on the "normal" request handling.

The client must send certain data, apart from the WBXML structure, with each request. This data is either present as "normal" GET variables or is sent as BASE64 encoded binary data sent in QUERY_STRING (see //**Horde_ActiveSync::getGetVars()**//). The format of this binary data is beyond the scope of this page, but the data is decoded in //**Horde_ActiveSync_Utils::decodeBase64()**//.

: Cmd : This is the command or request type. E.g., SYNC, PING, FOLDESYNC
: !DeviceId : This is a unique identifier for the client. This value is only unique to the client, not to the account. I.e., the same physical device/application will have the same !DeviceId. Multiple users can be associated with the same !DeviceId.

Flow is turned over to //**Horde_ActiveSync::handleRequest()**//. This is where the interesting stuff starts to happen.

+++Authentication+++ Authentication
First, we perform some checks, normalization, and call the versionCallback hook if it's present. If all is well, we finally attempt authentication. Authentication to Horde is a massive topic and it's only complicated more by layering ActiveSync on top of it. I will try to give a brief overview here. For a more detailed description of the general Horde authentication layer, see Jan's excellent series of posts on his [http://janschneider.de blog].

Thanks to broken clients, different supported authentication mechanisms and other idiosyncrasies, we need to perform some magic to make sure we have the user's credentials. For this, there is //Horde_ActiveSync_Credentials//. This class, when constructed and injected with the //Horde_ActiveSync// object, will have two properties set: ''username'' and ''password''. For the purposes of this page, we will assume a typical setup where the client properly sends credentials using HTTP BASIC and we are NOT using X509 certificates.

Once we have found the credentials, we call //**Horde_ActiveSync::authenticate()**//. This performs a few checks then ultimately passes control to //**Horde_Core_ActiveSync_Driver::authenticate()**//. Since some broken clients always send the email address as the username, plus the fact the Autodiscover requests ALWAYS use the email address, we need to normalize the username to the correct form. This is handled in //**Horde_Core_ActiveSync_Driver::getUsernameFromEmail()**// and is partially affected by the $GLOBALS['conf']['activesync']['autodiscovery'] setting (yes, this name is misleading since it is now used for more than just autodiscovery).

Once in //**Horde_Core_ActiveSync_Driver::authenticate()**// we use the //Horde_Core_ActiveSync_Auth// object that was injected when the driver was created to perform the actual authentication. The reason for this authentication wrapper is to allow for combinations of a "normal" Horde auth driver along with a transparent driver like X509 to support clients that allow for certificates along WITH credentials. For this page, we will assume a "normal" authentication.authentication - and as such, the actual authentication task is delegated to the 'base_driver' in //Horde_Core_ActiveSync_Auth//. That is, the authentication driver that is returned by 
<code>
$injector->getInstance('Horde_Core_Factory_Auth')->create()
</code>

+++ Device
Note that for BC reasons we refer to the client as "Device". We treat these two terms are interchangeable unless explicitly mentioned that we are talking about the physical device that the client is running on.
 
Once we have gotten authentication out the way, we perform some checks on the device/client. This includes things like making sure the device entry exists in our storage backend (or gets created if it doesn't), checking to see if the maximum protocol version we can handle has changed - and if so, notifying the client. We also call some hooks and callbacks to allow checking various permissions etc... All of this is handled in //Horde_ActiveSync::_handleDevice()//. At the end of that call (if successful), we know we have a device object available and the client is allowed to connect. Any failure here will result in an Exception being thrown and subsequently an appropriate HTTP error code to be sent back to the client.

+++ Provisioning
Now we take care of setting the provisioning flag and read the WBXML header in from the input stream. Provisioning support allows the server to take control of certain security settings on the client and also enables the ability to remote-wipe the client from the server. The provisioning flag is from Horde's permissions system and indicates the level of security required:

: Enable : This means the only clients that support full provisioning are allowed to connect to the server. Clients that don't support this or that have broken support (early Android clients) will NOT work.
: Allow : This means that we enforce provisioning on clients that support it, but also allow non-compliant clients to connect.
: Disable : No provisioning is done.

+++ Multipart
Next we check to see if the client accepts Multipart responses. This is typically used when fetching large amounts of data like e.g, an email attachment. This is indicated by the presence of a specific header or GET variable. We will assume this is not the case for this page.

+++ Command

Now we can take care of the actual command the client is attempting to perform. These are handled by Horde_ActiveSync_Request_* objects. Again, the name of these objects are a bit outdated and are planned to be renamed in H6. The object is instantiated and the handle() request is called. If all goes well, a value of true is returned and control is returned back to the RPC layer to finish up.

The Horde_ActiveSync_Request_* objects are responsible for enforcing the schema of whatever request is being handled, passing off any incoming additions/deletions/changes to the backend and delegating the responsibility for storing any data that needs to be persisted. This includes the collection state and the sync cache. This are explained in the section on the client life cycle.

There are almost 2 dozen different types of requests that handle things from synchronizing the folder structure to validating S/MIME certificates. The more common request objects are:

: Horde_ActiveSync_Request_Sync : This is the main code responsible for accepting and sending object changes, handling "hanging syncs" (which are SYNC requests that also act as PING requests) as well as handling certain options that the client sets (such as truncation, sync window etc...).

: Horde_ActiveSync_Request_Ping : This handles PING requests, which basically just continuously check for a change in the backend. It doesn't care what the change is, or how many there are. Much less resource intensive than a SYNC for this reason.

This is where the bulk of the action is performed and this description is of course a gross over-simplification. There are many other objects that are involved in handling the request - the responsibility of each of these objects are outlined in the General Library Structure section. 


++ Life Cycle of a Client.

What we will describe is the life cycle of a client-server pairing from the initial connection to be able to synchronize changes. Let's start with a fresh, never before connected client. The first thing that happens is the OPTIONS request. This essentially tells the server what protocol versions the client supports and the server responds with (among other things) the version that it will be using.