346 lines
13 KiB
C#
346 lines
13 KiB
C#
/// Copyright 2016 Kopano b.v.
|
|
///
|
|
/// This program is free software: you can redistribute it and/or modify
|
|
/// it under the terms of the GNU Affero General Public License, version 3,
|
|
/// as published by the Free Software Foundation.
|
|
///
|
|
/// This program is distributed in the hope that it will be useful,
|
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
|
/// GNU Affero General Public License for more details.
|
|
///
|
|
/// You should have received a copy of the GNU Affero General Public License
|
|
/// along with this program.If not, see<http://www.gnu.org/licenses/>.
|
|
///
|
|
/// Consult LICENSE file for details
|
|
|
|
using Acacia.ZPush.Connect.Soap;
|
|
using Acacia.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Security;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using Acacia.ZPush.Connect;
|
|
using Acacia.WBXML;
|
|
using Acacia.Stubs.OutlookWrappers;
|
|
using System.Text.RegularExpressions;
|
|
using Acacia.WBXML.ActiveSync;
|
|
using System.Security;
|
|
|
|
namespace Acacia.ZPush.Connect
|
|
{
|
|
/// <summary>
|
|
/// A connection to a ZPush server.
|
|
/// </summary>
|
|
public class ZPushConnection : IDisposable
|
|
{
|
|
#region SSL Error Handling
|
|
|
|
static ZPushConnection()
|
|
{
|
|
ServicePointManager.ServerCertificateValidationCallback = HandleCertificateError;
|
|
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
|
|
}
|
|
|
|
private static readonly Dictionary<string, bool> _allowCertificateErrors = new Dictionary<string, bool>();
|
|
private static bool HandleCertificateError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
|
{
|
|
if (sslPolicyErrors == SslPolicyErrors.None)
|
|
return true;
|
|
|
|
HttpWebRequest request = sender as HttpWebRequest;
|
|
if (request == null)
|
|
return false;
|
|
|
|
bool allow = false;
|
|
if (!_allowCertificateErrors.TryGetValue(request.Host, out allow))
|
|
{
|
|
if (GlobalOptions.INSTANCE.IgnoreCertificateErrors)
|
|
{
|
|
allow = true;
|
|
}
|
|
else
|
|
{
|
|
ThisAddIn.Instance.InvokeUI(() =>
|
|
{
|
|
allow = MessageBox.Show(
|
|
string.Format(Properties.Resources.SSLFailed_Body, request.Host),
|
|
Properties.Resources.SSLFailed_Title,
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Error
|
|
) == DialogResult.Yes;
|
|
});
|
|
}
|
|
_allowCertificateErrors.Add(request.Host, allow);
|
|
}
|
|
|
|
return allow;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Setup
|
|
|
|
private readonly ZPushAccount _account;
|
|
private readonly CancellationToken? _cancel;
|
|
|
|
public ZPushConnection(ZPushAccount account, CancellationToken? cancel)
|
|
{
|
|
if (account == null)
|
|
throw new ArgumentException("account cannot be null");
|
|
this._account = account;
|
|
this._cancel = cancel;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
|
|
public ZPushAccount Account { get { return _account; } }
|
|
|
|
#endregion
|
|
|
|
#region Web Services
|
|
|
|
public ZPushWebServiceInfo InfoService
|
|
{
|
|
get
|
|
{
|
|
return new ZPushWebServiceInfo(this);
|
|
}
|
|
}
|
|
|
|
public ZPushWebServiceDevice DeviceService
|
|
{
|
|
get
|
|
{
|
|
return new ZPushWebServiceDevice(this);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Requests
|
|
|
|
public object Execute(string url, RequestEncoder request)
|
|
{
|
|
// TODO: when other use of InitRequestHeader is removed, it can be merged here
|
|
using (HttpClient _client = CreateClient(_account))
|
|
{
|
|
// Content
|
|
using (HttpContent content = request.GetContent())
|
|
{
|
|
Logger.Instance.Trace(this, "Request: {0}", content.ReadAsStringAsync().Result);
|
|
using (HttpResponseMessage response = _client.PostAsync(url, content, _cancel ?? CancellationToken.None).Result)
|
|
using (HttpContent responseContent = response.Content)
|
|
{
|
|
Logger.Instance.Trace(this, "Response: {0}", responseContent.ReadAsStringAsync().Result);
|
|
return request.ParseResponse(url, responseContent.ReadAsStreamAsync().Result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static HttpClient CreateClient(ZPushAccount _account)
|
|
{
|
|
HttpClient _client = new HttpClient();
|
|
|
|
// Set up the authorization header
|
|
// TODO: it would be nice to let the system handle the SecureString for the password. However,
|
|
// when specifying credentials for an HttpClient, they are only used after a 401 is received
|
|
// on the first request, basically doubling the number of requests.
|
|
using (SecureString pass = _account.Account.Password)
|
|
{
|
|
var byteArray = Encoding.UTF8.GetBytes(_account.Account.UserName + ":" + pass.ConvertToUnsecureString());
|
|
var header = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
|
|
_client.DefaultRequestHeaders.Authorization = header;
|
|
}
|
|
|
|
// Client information
|
|
string pluginInfo = string.Format("{0}/{1}/{2}",
|
|
BuildVersions.VERSION, BuildVersions.REVISION, LibUtils.BuildTime.ToString(Constants.DATE_ISO_8601));
|
|
_client.DefaultRequestHeaders.Add(Constants.ZPUSH_HEADER_PLUGIN, pluginInfo);
|
|
|
|
// Other headers
|
|
// TODO: only for activesync
|
|
_client.DefaultRequestHeaders.Add("MS-ASProtocolVersion", "14.0");
|
|
_client.DefaultRequestHeaders.Add("Accept", "*/*");
|
|
|
|
return _client;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ActiveSync
|
|
// TODO: this needs an update to using Soap-style request handling
|
|
|
|
public class Response : IDisposable
|
|
{
|
|
public bool Success
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public WBXMLDocument Body
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public string GABName
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public ZPushCapabilities Capabilities
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public string ZPushVersion
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public string SignaturesHash
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private string GetStringHeader(HttpResponseMessage response, string name)
|
|
{
|
|
IEnumerable<string> values;
|
|
if (!response.Headers.TryGetValues(name, out values))
|
|
return null;
|
|
|
|
return string.Join("", values);
|
|
}
|
|
|
|
public Response(HttpResponseMessage response)
|
|
{
|
|
Logger.Instance.Trace(this, "Response received: {0} {1}\n{2}", (int)response.StatusCode, response.ReasonPhrase, response.Headers);
|
|
|
|
// Check for ZPush headers
|
|
// GAB name is now hex encoded, but also support old-style for transition
|
|
string gabNameOrig = GetStringHeader(response, Constants.ZPUSH_HEADER_GAB_NAME);
|
|
if (gabNameOrig != null && new Regex("^[0-9a-fA-F]+$").IsMatch(gabNameOrig))
|
|
GABName = StringUtil.HexToUtf8(gabNameOrig);
|
|
else
|
|
GABName = gabNameOrig;
|
|
|
|
Capabilities = ZPushCapabilities.Parse(GetStringHeader(response, Constants.ZPUSH_HEADER_CAPABILITIES));
|
|
ZPushVersion = GetStringHeader(response, Constants.ZPUSH_HEADER_VERSION);
|
|
SignaturesHash = GetStringHeader(response, Constants.ZPUSH_HEADER_SIGNATURES_HASH);
|
|
|
|
// Check for success
|
|
Success = response.IsSuccessStatusCode;
|
|
if (Success)
|
|
{
|
|
// Parse the body
|
|
using (HttpContent responseContent = response.Content)
|
|
{
|
|
byte[] result = responseContent.ReadAsByteArrayAsync().Result;
|
|
Body = new WBXMLDocument();
|
|
Body.VersionNumber = 1.3;
|
|
Body.TagCodeSpace = new ActiveSyncCodeSpace();
|
|
Body.Encoding = Encoding.UTF8;
|
|
Body.LoadBytes(result);
|
|
}
|
|
}
|
|
|
|
Logger.Instance.Trace(this, "Response parsed: {0}", Body == null ? "Failure" : Body.ToXMLString());
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class Request : DisposableWrapper
|
|
{
|
|
private const string ACTIVESYNC_URL = "https://{0}/Microsoft-Server-ActiveSync?DeviceId={1}&Cmd={2}&User={3}&DeviceType={4}";
|
|
|
|
private readonly ZPushAccount _account;
|
|
private readonly CancellationToken _cancel;
|
|
private HttpClient _client;
|
|
|
|
public Request(ZPushAccount account, CancellationToken cancel)
|
|
{
|
|
this._account = account;
|
|
this._cancel = cancel;
|
|
this._client = CreateClient(account);
|
|
}
|
|
|
|
protected override void DoRelease()
|
|
{
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
public Response Execute(ActiveSync.RequestBase request)
|
|
{
|
|
string url = string.Format(ACTIVESYNC_URL,
|
|
_account.Account.ServerURL,
|
|
Uri.EscapeDataString(_account.Account.DeviceId),
|
|
request.Command,
|
|
Uri.EscapeDataString(_account.Account.UserName),
|
|
"WindowsOutlook");
|
|
|
|
// Construct the body
|
|
WBXMLDocument doc = new WBXMLDocument();
|
|
doc.LoadXml(request.Body);
|
|
doc.VersionNumber = 1.3;
|
|
doc.TagCodeSpace = new ActiveSyncCodeSpace();
|
|
doc.Encoding = Encoding.UTF8;
|
|
byte[] contentBody = doc.GetBytes();
|
|
|
|
using (HttpContent content = new ByteArrayContent(contentBody))
|
|
{
|
|
Logger.Instance.Trace(this, "Sending request: {0} -> {1}", _account.Account.ServerURL, doc.ToXMLString());
|
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.ms-sync.wbxml");
|
|
string caps = ZPushCapabilities.Client.ToString();
|
|
Logger.Instance.Trace(this, "Sending request: {0} -> {1}: {2}", _account.Account.ServerURL, caps, doc.ToXMLString());
|
|
content.Headers.Add(Constants.ZPUSH_HEADER_CLIENT_CAPABILITIES, caps);
|
|
using (HttpResponseMessage response = _client.PostAsync(url, content, _cancel).Result)
|
|
{
|
|
return new Response(response);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public ResponseType Execute<ResponseType>(ActiveSync.Request<ResponseType> request)
|
|
where ResponseType : ActiveSync.Response, new()
|
|
{
|
|
using (Request requestMessage = new Request(_account, _cancel ?? CancellationToken.None))
|
|
{
|
|
using (Response response = requestMessage.Execute(request))
|
|
{
|
|
ResponseType typed = new ResponseType();
|
|
typed.ParseResponse(request, response);
|
|
return typed;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
}
|