From 3bbc92b11589e16799bcae37f65d64b63d2d47a7 Mon Sep 17 00:00:00 2001 From: baywet Date: Fri, 22 Dec 2017 09:36:47 -0500 Subject: [PATCH 1/4] - got rid of linting errors - code optimizations - removed exceptions re-throw to conserve stack traces --- UCWASDK/UCWASDK/Services/HttpService.cs | 25 +++------ UCWASDK/UCWASDK/UCWAClient.cs | 68 +++++++++---------------- 2 files changed, 29 insertions(+), 64 deletions(-) diff --git a/UCWASDK/UCWASDK/Services/HttpService.cs b/UCWASDK/UCWASDK/Services/HttpService.cs index 624ac92..60bccd0 100644 --- a/UCWASDK/UCWASDK/Services/HttpService.cs +++ b/UCWASDK/UCWASDK/Services/HttpService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; @@ -22,7 +21,7 @@ static public class HttpService // Store HttpClient per request Uri static ConcurrentDictionary clientPool = new ConcurrentDictionary(); static private ExceptionMappingService exceptionMappingService = new ExceptionMappingService(); - + static public async Task Get(UCWAHref href, string version = "2.0") where T : UCWAModelBase { if (href == null || string.IsNullOrEmpty(href.Href)) @@ -32,7 +31,7 @@ static public async Task Get(UCWAHref href, string version = "2.0") where } static public async Task> GetList(UCWAHref[] hrefs, string version = "2.0") where T : UCWAModelBase { - if (hrefs == null || hrefs.Count() == 0) + if (hrefs == null || !hrefs.Any()) return null; List list = new List(); @@ -281,7 +280,7 @@ static private void GetPGuid(JToken jToken) foreach (var jtoken in jToken as JArray) { GetPGuid(jtoken); } else { - var pGuidObj = jToken.Values().Where(x => x.ToString() == "please pass this in a PUT request").FirstOrDefault(); + var pGuidObj = jToken.Values().FirstOrDefault(x => x.ToString() == "please pass this in a PUT request"); if (pGuidObj != null) jToken["pGuid"] = (pGuidObj.Parent as JProperty).Name; if (jToken["_embedded"] != null) @@ -304,22 +303,10 @@ static private async Task GetClient(string uri, string version) // If we want to consider concurrency in the future, we may implement lock, but as this is client library, I just keep it simple at the moment. client = new HttpClient(); client.DefaultRequestHeaders.TryAddWithoutValidation("X-MS-RequiresMinResourceVersion", version); - try - { - if (!clientPool.TryAdd(hostname, client)) - { - // As the pool contains the key already, get the HttpClient from the pool. - client = clientPool[hostname]; - } - } - catch (OverflowException) - { - // The dictionary already contains the maximum number of elements(MaxValue) - throw; - } - catch(Exception) + if (!clientPool.TryAdd(hostname, client)) { - throw; + // As the pool contains the key already, get the HttpClient from the pool. + client = clientPool[hostname]; } } diff --git a/UCWASDK/UCWASDK/UCWAClient.cs b/UCWASDK/UCWASDK/UCWAClient.cs index cab8a5c..0db6492 100644 --- a/UCWASDK/UCWASDK/UCWAClient.cs +++ b/UCWASDK/UCWASDK/UCWAClient.cs @@ -683,22 +683,22 @@ public async Task SignIn(Availability availability, bool supportMessage, bool su bool supportPlainText, bool supportHtmlFormat, string phoneNumber, bool keepAlive) { if (application == null) - throw new Exception("You need to initialize and subscribe the event before starting."); + throw new InvalidOperationException("You need to initialize and subscribe the event before starting."); await application.Me.MakeMeAvailable(availability, supportMessage, supportAudio, supportPlainText, supportHtmlFormat, phoneNumber); application = await application.Get(); if (keepAlive) - KeepAlive(60); + Parallel.Invoke(async () => await KeepAlive(60)); - MonitorEvent(); + Parallel.Invoke(async () => await MonitorEvent()); } /// /// Keep the status by sending ReportActivity. /// /// Interval to send ReportActivity in seconds. Default value is 60. - private async void KeepAlive(int durationInSeconds = 60) + private async Task KeepAlive(int durationInSeconds = 60) { while (Me != null) { @@ -739,7 +739,7 @@ public async Task UnSubscribeContactsChange(params string[] sips) PresenceSubscriptions presenceSubscriptions = await application.People.GetPresenceSubscriptions(); foreach (var sip in sips) { - PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.Where(x => x.Id == sip).FirstOrDefault(); + PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.FirstOrDefault(x => x.Id == sip); if (presenceSubscription != null) await presenceSubscription.Delete(); } @@ -800,8 +800,8 @@ public async Task GetConversation(string subject = "", string titl return null; Conversation conversation = string.IsNullOrEmpty(subject) ? - convs.Where(x => x.Title.ToLower() == title.ToLower()).FirstOrDefault() : - convs.Where(x => x.Subject.ToLower() == subject.ToLower()).FirstOrDefault(); + convs.FirstOrDefault(x => x.Title.ToLower() == title.ToLower()) : + convs.FirstOrDefault(x => x.Subject.ToLower() == subject.ToLower()); return conversation; } @@ -826,7 +826,7 @@ public async Task StartMessaging(string sip, string subject = "", Importance imp /// public async Task StartOnlineMeeting(string subject, Importance importance = Importance.Normal) { - string location = await application.Communication.StartOnlineMeeting(subject, importance); + await application.Communication.StartOnlineMeeting(subject, importance); } /// @@ -971,24 +971,17 @@ public async Task ReplyMessage(string text, OnlineMeetingInvitation onlineMeetin private async Task CreateApplication(string agentName = "myAgent", string language = "en-US") { - try - { - User user = await GetUserDiscoverUri(); - if (user == null) - return false; + User user = await GetUserDiscoverUri(); + if (user == null) + return false; - application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); + application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); - // Get host address - Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; + // Get host address + Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; - eventUri = application.Links.Events; - return true; - } - catch (Exception ex) - { - throw ex; - } + eventUri = application.Links.Events; + return true; } private async Task GetUserDiscoverUri() @@ -1025,10 +1018,6 @@ private async Task GetUserDiscoverUri() throw; } } - catch (Exception ex) - { - throw; - } if (response.IsSuccessStatusCode) { var root = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Include }); @@ -1055,14 +1044,8 @@ public async Task Refresh() private async Task GetEvent() { - while (true) - { - if (string.IsNullOrEmpty(eventUri)) - { - return null; - } - using (HttpClient client = new HttpClient()) - { + while (!string.IsNullOrEmpty(eventUri)) + using (var client = new HttpClient()) try { var ucwaEvent = await HttpService.Get(eventUri); @@ -1070,28 +1053,23 @@ private async Task GetEvent() if (ucwaEvent == null) await Task.Delay(1000); - if (!string.IsNullOrEmpty(ucwaEvent.Links.Resync)) + if (!string.IsNullOrEmpty(ucwaEvent?.Links?.Resync)) { eventUri = ucwaEvent.Links.Resync; ucwaEvent = await GetEvent(); } - eventUri = ucwaEvent.Links.Next; + eventUri = ucwaEvent?.Links?.Next; return ucwaEvent; } - catch (TaskCanceledException ex) + catch (TaskCanceledException) { await Task.Delay(1000); } - catch (Exception ex) - { - throw ex; - } - } - } + return null; } - private async void MonitorEvent() + private async Task MonitorEvent() { while (true) { From da4e20ccd010cfc9c6571987a08ea71e80fe0f82 Mon Sep 17 00:00:00 2001 From: baywet Date: Fri, 22 Dec 2017 09:39:57 -0500 Subject: [PATCH 2/4] Revert "- got rid of linting errors" This reverts commit 3bbc92b11589e16799bcae37f65d64b63d2d47a7. --- UCWASDK/UCWASDK/Services/HttpService.cs | 25 ++++++--- UCWASDK/UCWASDK/UCWAClient.cs | 68 ++++++++++++++++--------- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/UCWASDK/UCWASDK/Services/HttpService.cs b/UCWASDK/UCWASDK/Services/HttpService.cs index 60bccd0..624ac92 100644 --- a/UCWASDK/UCWASDK/Services/HttpService.cs +++ b/UCWASDK/UCWASDK/Services/HttpService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; @@ -21,7 +22,7 @@ static public class HttpService // Store HttpClient per request Uri static ConcurrentDictionary clientPool = new ConcurrentDictionary(); static private ExceptionMappingService exceptionMappingService = new ExceptionMappingService(); - + static public async Task Get(UCWAHref href, string version = "2.0") where T : UCWAModelBase { if (href == null || string.IsNullOrEmpty(href.Href)) @@ -31,7 +32,7 @@ static public async Task Get(UCWAHref href, string version = "2.0") where } static public async Task> GetList(UCWAHref[] hrefs, string version = "2.0") where T : UCWAModelBase { - if (hrefs == null || !hrefs.Any()) + if (hrefs == null || hrefs.Count() == 0) return null; List list = new List(); @@ -280,7 +281,7 @@ static private void GetPGuid(JToken jToken) foreach (var jtoken in jToken as JArray) { GetPGuid(jtoken); } else { - var pGuidObj = jToken.Values().FirstOrDefault(x => x.ToString() == "please pass this in a PUT request"); + var pGuidObj = jToken.Values().Where(x => x.ToString() == "please pass this in a PUT request").FirstOrDefault(); if (pGuidObj != null) jToken["pGuid"] = (pGuidObj.Parent as JProperty).Name; if (jToken["_embedded"] != null) @@ -303,10 +304,22 @@ static private async Task GetClient(string uri, string version) // If we want to consider concurrency in the future, we may implement lock, but as this is client library, I just keep it simple at the moment. client = new HttpClient(); client.DefaultRequestHeaders.TryAddWithoutValidation("X-MS-RequiresMinResourceVersion", version); - if (!clientPool.TryAdd(hostname, client)) + try + { + if (!clientPool.TryAdd(hostname, client)) + { + // As the pool contains the key already, get the HttpClient from the pool. + client = clientPool[hostname]; + } + } + catch (OverflowException) + { + // The dictionary already contains the maximum number of elements(MaxValue) + throw; + } + catch(Exception) { - // As the pool contains the key already, get the HttpClient from the pool. - client = clientPool[hostname]; + throw; } } diff --git a/UCWASDK/UCWASDK/UCWAClient.cs b/UCWASDK/UCWASDK/UCWAClient.cs index 0db6492..cab8a5c 100644 --- a/UCWASDK/UCWASDK/UCWAClient.cs +++ b/UCWASDK/UCWASDK/UCWAClient.cs @@ -683,22 +683,22 @@ public async Task SignIn(Availability availability, bool supportMessage, bool su bool supportPlainText, bool supportHtmlFormat, string phoneNumber, bool keepAlive) { if (application == null) - throw new InvalidOperationException("You need to initialize and subscribe the event before starting."); + throw new Exception("You need to initialize and subscribe the event before starting."); await application.Me.MakeMeAvailable(availability, supportMessage, supportAudio, supportPlainText, supportHtmlFormat, phoneNumber); application = await application.Get(); if (keepAlive) - Parallel.Invoke(async () => await KeepAlive(60)); + KeepAlive(60); - Parallel.Invoke(async () => await MonitorEvent()); + MonitorEvent(); } /// /// Keep the status by sending ReportActivity. /// /// Interval to send ReportActivity in seconds. Default value is 60. - private async Task KeepAlive(int durationInSeconds = 60) + private async void KeepAlive(int durationInSeconds = 60) { while (Me != null) { @@ -739,7 +739,7 @@ public async Task UnSubscribeContactsChange(params string[] sips) PresenceSubscriptions presenceSubscriptions = await application.People.GetPresenceSubscriptions(); foreach (var sip in sips) { - PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.FirstOrDefault(x => x.Id == sip); + PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.Where(x => x.Id == sip).FirstOrDefault(); if (presenceSubscription != null) await presenceSubscription.Delete(); } @@ -800,8 +800,8 @@ public async Task GetConversation(string subject = "", string titl return null; Conversation conversation = string.IsNullOrEmpty(subject) ? - convs.FirstOrDefault(x => x.Title.ToLower() == title.ToLower()) : - convs.FirstOrDefault(x => x.Subject.ToLower() == subject.ToLower()); + convs.Where(x => x.Title.ToLower() == title.ToLower()).FirstOrDefault() : + convs.Where(x => x.Subject.ToLower() == subject.ToLower()).FirstOrDefault(); return conversation; } @@ -826,7 +826,7 @@ public async Task StartMessaging(string sip, string subject = "", Importance imp /// public async Task StartOnlineMeeting(string subject, Importance importance = Importance.Normal) { - await application.Communication.StartOnlineMeeting(subject, importance); + string location = await application.Communication.StartOnlineMeeting(subject, importance); } /// @@ -971,17 +971,24 @@ public async Task ReplyMessage(string text, OnlineMeetingInvitation onlineMeetin private async Task CreateApplication(string agentName = "myAgent", string language = "en-US") { - User user = await GetUserDiscoverUri(); - if (user == null) - return false; + try + { + User user = await GetUserDiscoverUri(); + if (user == null) + return false; - application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); + application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); - // Get host address - Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; + // Get host address + Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; - eventUri = application.Links.Events; - return true; + eventUri = application.Links.Events; + return true; + } + catch (Exception ex) + { + throw ex; + } } private async Task GetUserDiscoverUri() @@ -1018,6 +1025,10 @@ private async Task GetUserDiscoverUri() throw; } } + catch (Exception ex) + { + throw; + } if (response.IsSuccessStatusCode) { var root = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Include }); @@ -1044,8 +1055,14 @@ public async Task Refresh() private async Task GetEvent() { - while (!string.IsNullOrEmpty(eventUri)) - using (var client = new HttpClient()) + while (true) + { + if (string.IsNullOrEmpty(eventUri)) + { + return null; + } + using (HttpClient client = new HttpClient()) + { try { var ucwaEvent = await HttpService.Get(eventUri); @@ -1053,23 +1070,28 @@ private async Task GetEvent() if (ucwaEvent == null) await Task.Delay(1000); - if (!string.IsNullOrEmpty(ucwaEvent?.Links?.Resync)) + if (!string.IsNullOrEmpty(ucwaEvent.Links.Resync)) { eventUri = ucwaEvent.Links.Resync; ucwaEvent = await GetEvent(); } - eventUri = ucwaEvent?.Links?.Next; + eventUri = ucwaEvent.Links.Next; return ucwaEvent; } - catch (TaskCanceledException) + catch (TaskCanceledException ex) { await Task.Delay(1000); } - return null; + catch (Exception ex) + { + throw ex; + } + } + } } - private async Task MonitorEvent() + private async void MonitorEvent() { while (true) { From b80f1d31ac34fc4554384e666a4c88c13ef5c904 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Dec 2017 13:12:48 -0500 Subject: [PATCH 3/4] fixed a bug where null dial in information would make query on the region property always fail (#40) Thanks for the null check! --- UCWASDK/UCWASDK/Models/PhoneDialInInformation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UCWASDK/UCWASDK/Models/PhoneDialInInformation.cs b/UCWASDK/UCWASDK/Models/PhoneDialInInformation.cs index 3650deb..cf4ffe9 100644 --- a/UCWASDK/UCWASDK/Models/PhoneDialInInformation.cs +++ b/UCWASDK/UCWASDK/Models/PhoneDialInInformation.cs @@ -35,7 +35,7 @@ public class PhoneDialInInformation : UCWAModelBaseLink internal InternalEmbedded Embedded { get; set; } [JsonIgnore] - public DialInRegion[] DialInRegion { get { return Embedded.dialInRegion; } } + public DialInRegion[] DialInRegion { get { return Embedded?.dialInRegion; } } internal class InternalEmbedded { From 82056e40cd3d23cb045d37f20751c830f0f0e716 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Dec 2017 13:13:57 -0500 Subject: [PATCH 4/4] Bugfix/authentication expiration not handled (#39) * Revert "Revert "- got rid of linting errors"" This reverts commit da4e20ccd010cfc9c6571987a08ea71e80fe0f82. * implemented btter autodiscovery to avoid unnecessary calls with https exceptions * implemented authentication expiration exception detection for better retry policy * implemented anonymous requests support and redirect get in anonymous for better respect with specification * implemented authentication regeneration when is expires * imrpoved the way tokens are requested * cleaned unused key * adding default content type to anonymous get --- UCWASDK/UCWASDK/Models/Root.cs | 6 +- UCWASDK/UCWASDK/Models/UCWAErrors.cs | 17 ++- .../Services/ExceptionMappingService.cs | 45 ++++++ UCWASDK/UCWASDK/Services/HttpService.cs | 132 ++++++++++-------- UCWASDK/UCWASDK/Services/Settings.cs | 9 +- UCWASDK/UCWASDK/UCWAClient.cs | 81 ++++------- 6 files changed, 171 insertions(+), 119 deletions(-) diff --git a/UCWASDK/UCWASDK/Models/Root.cs b/UCWASDK/UCWASDK/Models/Root.cs index b7454b1..33ee44a 100644 --- a/UCWASDK/UCWASDK/Models/Root.cs +++ b/UCWASDK/UCWASDK/Models/Root.cs @@ -23,17 +23,17 @@ internal class InternalLinks [JsonProperty("redirect")] internal UCWAHref redirect { get; set; } - + [JsonProperty("user")] internal UCWAHref user { get; set; } - + [JsonProperty("xframe")] internal UCWAHref xframe { get; set; } } public async Task GetRedirect() { - return Links.redirect == null ? null : await HttpService.Get(Links.redirect); + return Links.redirect == null ? null : await HttpService.Get(Links.redirect, anonymous: true); } public async Task GetUser() diff --git a/UCWASDK/UCWASDK/Models/UCWAErrors.cs b/UCWASDK/UCWASDK/Models/UCWAErrors.cs index 1bb96cb..6c7109b 100644 --- a/UCWASDK/UCWASDK/Models/UCWAErrors.cs +++ b/UCWASDK/UCWASDK/Models/UCWAErrors.cs @@ -194,5 +194,20 @@ public VersionNotSupportedException(string message, Exception innerException) : private bool _isTransient; public bool IsTransient { get => _isTransient; set => _isTransient = value; } } - + public class AuthenticationExpiredException : Exception, IUCWAException + { + public override string Message { get { return $"Authentication information : {nameof(TrustedIssuers)} {TrustedIssuers}, {nameof(ClientId)} {ClientId}, {nameof(GrantType)} {GrantType}, {nameof(TokenUri)} {TokenUri}, {nameof(AuthorizationUri)} {AuthorizationUri}, {base.Message}"; } } + public AuthenticationExpiredException() : base() { } + public AuthenticationExpiredException(string message) : base(message) { } + public AuthenticationExpiredException(string message, Exception innerException) : base(message, innerException) { } + private Reason _reason; + public Reason Reason { get => _reason; set => _reason = value; } + private bool _isTransient; + public bool IsTransient { get => _isTransient; set => _isTransient = value; } + public string TrustedIssuers { get; internal set; } + public string ClientId { get; internal set; } + public string TokenUri { get; internal set; } + public string GrantType { get; internal set; } + public string AuthorizationUri { get; internal set; } + } } diff --git a/UCWASDK/UCWASDK/Services/ExceptionMappingService.cs b/UCWASDK/UCWASDK/Services/ExceptionMappingService.cs index 3548ff5..d28d751 100644 --- a/UCWASDK/UCWASDK/Services/ExceptionMappingService.cs +++ b/UCWASDK/UCWASDK/Services/ExceptionMappingService.cs @@ -2,13 +2,41 @@ using Microsoft.Skype.UCWA.Models; using Newtonsoft.Json; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; namespace Microsoft.Skype.UCWA.Services { internal class ExceptionMappingService { + private const string matchKeyKey = "key"; + private const string matchValueKey = "value"; + private const string trustedIssuersKey = "trusted_issuers"; + private const string clientIdKey = "client_id"; + private const string grantTypeKey = "grant_type"; + private const string tokenKey = "href"; + private const string authorizationKey = "authorization_uri"; + private static Regex authenticationMatchingRegex = new Regex($@"(?<{matchKeyKey}>[\w]+)=""(?<{matchValueKey}>[\w -@\.:/,\*]+)""?"); + private Dictionary getAuthenticationHeaderValues(HttpResponseHeaders headers) + { + var value = new Dictionary(); + var matches = headers.WwwAuthenticate.SelectMany( x => x.Parameter.Split(new string[] { "\"," }, StringSplitOptions.RemoveEmptyEntries)).Select(x => authenticationMatchingRegex.Match(x)).Where(x => x.Success); + foreach (var match in matches) + { + var mtch = match; + do + { + value.Add(match.Groups[matchKeyKey].Value, match.Groups[matchValueKey].Value); + mtch = mtch.NextMatch(); + } + while (mtch.Success); + } + return value; + } internal Exception GetExceptionFromHttpStatusCode(HttpResponseMessage response, string error) {// we can start by guessing what kind of error it is by relying on the protocol first https://ucwa.skype.com/documentation/ProgrammingConcepts-Errors var reason = TryDeserializeReason(error); @@ -41,6 +69,23 @@ internal Exception GetExceptionFromHttpStatusCode(HttpResponseMessage response, return new PreconditionFailedException(reason?.Message ?? error) { Reason = reason, IsTransient = false }; #endregion #region nontransient + case HttpStatusCode.Unauthorized: + if (response.Headers.WwwAuthenticate.Any()) + { + var authenticationHeaderValues = getAuthenticationHeaderValues(response.Headers); + return new AuthenticationExpiredException(reason?.Message ?? error) + { + Reason = reason, + IsTransient = true, + ClientId = authenticationHeaderValues[clientIdKey], + GrantType = authenticationHeaderValues[grantTypeKey], + TokenUri = authenticationHeaderValues[tokenKey], + TrustedIssuers = authenticationHeaderValues[trustedIssuersKey], + AuthorizationUri = authenticationHeaderValues[authorizationKey], + }; + } + else + return new UnauthorizedAccessException(error); case HttpStatusCode.Forbidden: return new UnauthorizedAccessException(error); case HttpStatusCode.RequestEntityTooLarge: diff --git a/UCWASDK/UCWASDK/Services/HttpService.cs b/UCWASDK/UCWASDK/Services/HttpService.cs index 624ac92..75507bb 100644 --- a/UCWASDK/UCWASDK/Services/HttpService.cs +++ b/UCWASDK/UCWASDK/Services/HttpService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; @@ -19,62 +18,79 @@ namespace Microsoft.Skype.UCWA.Services /// static public class HttpService { + private const string defaultVersion = "2.0"; // Store HttpClient per request Uri static ConcurrentDictionary clientPool = new ConcurrentDictionary(); + static HttpClient _anonymousHttpClient; + static HttpClient AnonymousHttpClient + { + get + { + if(_anonymousHttpClient == null) + { + _anonymousHttpClient = new HttpClient(); + _anonymousHttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json"); + } + return _anonymousHttpClient; + } + } static private ExceptionMappingService exceptionMappingService = new ExceptionMappingService(); - - static public async Task Get(UCWAHref href, string version = "2.0") where T : UCWAModelBase + + static public async Task Get(UCWAHref href, string version = defaultVersion, bool anonymous = false) where T : UCWAModelBase { if (href == null || string.IsNullOrEmpty(href.Href)) return default(T); - return await Get(href.Href, version); + return await Get(href.Href, version, anonymous); } - static public async Task> GetList(UCWAHref[] hrefs, string version = "2.0") where T : UCWAModelBase + static public async Task> GetList(UCWAHref[] hrefs, string version = defaultVersion, bool anonymous = false) where T : UCWAModelBase { - if (hrefs == null || hrefs.Count() == 0) + if (hrefs == null || !hrefs.Any()) return null; List list = new List(); - foreach (var href in hrefs) { if (!string.IsNullOrEmpty(href.Href)) list.Add(await Get(href.Href, version)); } + foreach (var href in hrefs) { if (!string.IsNullOrEmpty(href.Href)) list.Add(await Get(href.Href, version, anonymous)); } return list; } - static public async Task Get(string uri, string version = "2.0") where T : UCWAModelBase + static public async Task Get(string uri, string version = defaultVersion, bool anonymous = false) where T : UCWAModelBase { uri = EnsureUriContainsHttp(uri); - var client = await GetClient(uri, version); - return await ExecuteHttpCallAndRetry(() => client.GetAsync(uri), async (response) => + return await ExecuteHttpCallAndRetry(() => GetInternal(uri, version, anonymous), async (response) => { var jObject = JObject.Parse(await response.Content.ReadAsStringAsync()); GetPGuid(jObject as JToken); return JsonConvert.DeserializeObject(jObject.ToString()); }); } - static public async Task GetBinary(UCWAHref href, string version = "2.0") + static public async Task GetInternal(string uri, string version = defaultVersion, bool anonymous = false) + { + var client = await GetClient(uri, version, anonymous); + return await client.GetAsync(uri); + } + static public async Task GetBinary(UCWAHref href, string version = defaultVersion, bool anonymous = false) { if (href == null || string.IsNullOrEmpty(href.Href)) return null; var uri = EnsureUriContainsHttp(href.Href); - var client = await GetClient(uri, version); - return await ExecuteHttpCallAndRetry(() => client.GetAsync(uri), async (response) => + return await ExecuteHttpCallAndRetry(() => GetInternal(uri, version, anonymous), async (response) => { return await response.Content.ReadAsByteArrayAsync(); }); } - static public async Task Post(UCWAHref href, object body, string version = "2.0") + static public async Task Post(UCWAHref href, object body, string version = defaultVersion, bool anonymous = false) { if (href == null || string.IsNullOrEmpty(href.Href)) return string.Empty; - return await Post(href.Href, body, version); + return await Post(href.Href, body, version, anonymous); } - static public async Task Post(string uri, object body, string version = "2.0") + static public async Task Post(string uri, object body, string version = defaultVersion, bool anonymous = false) { - return await ExecuteHttpCallAndRetry(() => PostInternal(uri, body, version), (response) => + return await ExecuteHttpCallAndRetry(() => PostInternal(uri, body, version, anonymous), (response) => { if (response.StatusCode == HttpStatusCode.Created) return response.Headers.Location.ToString(); @@ -82,51 +98,56 @@ static public async Task Post(string uri, object body, string version = return string.Empty; }); } - static public async Task Post(UCWAHref href, object body, string version = "2.0") + static public async Task Post(UCWAHref href, object body, string version = defaultVersion, bool anonymous = false) { if (href == null || string.IsNullOrEmpty(href.Href)) return default(T); - return await Post(href.Href, body, version); + return await Post(href.Href, body, version, anonymous); } - static public async Task Post(string uri, object body, string version = "2.0") + static public async Task Post(string uri, object body, string version = defaultVersion, bool anonymous = false) { - return await ExecuteHttpCallAndRetry(() => PostInternal(uri, body, version), async (response) => + return await ExecuteHttpCallAndRetry(() => PostInternal(uri, body, version, anonymous), async (response) => { var jObject = JObject.Parse(await response.Content.ReadAsStringAsync()); GetPGuid(jObject as JToken); return JsonConvert.DeserializeObject(jObject.ToString()); }); } - static public async Task Put(string uri, UCWAModelBase body, string version = "2.0") + static public async Task Put(string uri, UCWAModelBase body, string version = defaultVersion, bool anonymous = false) { - await ExecuteHttpCallAndRetry(() => PutInternal(uri, body, version)); + await ExecuteHttpCallAndRetry(() => PutInternal(uri, body, version, anonymous)); } - static public async Task Put(string uri, UCWAModelBase body, string version = "2.0") + static public async Task Put(string uri, UCWAModelBase body, string version = defaultVersion, bool anonymous = false) { - return await ExecuteHttpCallAndRetry(() => PutInternal(uri, body, version), async (response) => + return await ExecuteHttpCallAndRetry(() => PutInternal(uri, body, version, anonymous), async (response) => { var jObject = JObject.Parse(await response.Content.ReadAsStringAsync()); GetPGuid(jObject as JToken); return JsonConvert.DeserializeObject(jObject.ToString()); }); } - static public async Task Delete(string uri, string version = "2.0") + static public async Task Delete(string uri, string version = defaultVersion, bool anonymous = false) { if (string.IsNullOrEmpty(uri)) return; uri = EnsureUriContainsHttp(uri); - var client = await GetClient(uri, version); - await ExecuteHttpCallAndRetry(() => client.DeleteAsync(uri)); + await ExecuteHttpCallAndRetry(() => DeleteInternal(uri, version, anonymous)); + } + static public async Task DeleteInternal(string uri, string version = defaultVersion, bool anonymous = false) + { + var client = await GetClient(uri, version, anonymous); + return await client.DeleteAsync(uri); } static public void DisposeHttpClients() { foreach (var client in clientPool.Values) - { client.Dispose(); - } + clientPool.Clear(); + _anonymousHttpClient?.Dispose(); + _anonymousHttpClient = null; } static private string EnsureUriContainsHttp(string uri) @@ -135,7 +156,7 @@ static private string EnsureUriContainsHttp(string uri) uri = Settings.Host + uri; return uri; } - static private async Task PostInternal(string uri, object body, string version = "") + static private async Task PostInternal(string uri, object body, string version = defaultVersion, bool anonymous = false) { if (string.IsNullOrEmpty(uri)) return new HttpResponseMessage(); @@ -154,7 +175,7 @@ static private async Task PostInternal(string uri, object b body = jobject; } - var client = await GetClient(uri, version); + var client = await GetClient(uri, version, anonymous); HttpResponseMessage response = null; if (body is string) @@ -168,14 +189,14 @@ static private async Task PostInternal(string uri, object b } return response; } - static private async Task PutInternal(string uri, UCWAModelBase body, string version = "") + static private async Task PutInternal(string uri, UCWAModelBase body, string version = defaultVersion, bool anonymous = false) { if (string.IsNullOrEmpty(uri)) return new HttpResponseMessage(); uri = EnsureUriContainsHttp(uri); - var client = await GetClient(uri, version); + var client = await GetClient(uri, version, anonymous); JsonSerializer serializer = new JsonSerializer() { DefaultValueHandling = DefaultValueHandling.Ignore }; serializer.Converters.Add(new StringEnumConverter()); JObject jobject = JObject.FromObject(body, serializer); @@ -208,6 +229,8 @@ static private async Task ExecuteHttpCallAndRetry(Func } catch (Exception ex) when (ex is IUCWAException && (ex as IUCWAException).IsTransient) { + if (ex is AuthenticationExpiredException) + DisposeHttpClients(); lastException = ex;// memorizing the last transient exception in case we still encounter it but run out of retries await Task.Delay(Settings.UCWAClient.TransientErrorHandlingPolicy.GetNextErrorWaitTimeInMs(retryCount)); retryCount++; @@ -233,6 +256,8 @@ static private async Task ExecuteHttpCallAndRetry(Func ExecuteHttpCallAndRetry(Func x.ToString() == "please pass this in a PUT request").FirstOrDefault(); + var pGuidObj = jToken.Values().FirstOrDefault(x => x.ToString() == "please pass this in a PUT request"); if (pGuidObj != null) jToken["pGuid"] = (pGuidObj.Parent as JProperty).Name; if (jToken["_embedded"] != null) @@ -291,41 +318,34 @@ static private void GetPGuid(JToken jToken) /// /// Returns same HttpClient instance per Uri hostname. /// - static private async Task GetClient(string uri, string version) + static private async Task GetClient(string uri, string version, bool anonymous = false) { HttpClient client; var hostname = new Uri(uri).Host; - if (clientPool.ContainsKey(hostname)) - { + if (anonymous) + client = AnonymousHttpClient; + else if (clientPool.ContainsKey(hostname)) client = clientPool[hostname]; - } else { // If we want to consider concurrency in the future, we may implement lock, but as this is client library, I just keep it simple at the moment. client = new HttpClient(); - client.DefaultRequestHeaders.TryAddWithoutValidation("X-MS-RequiresMinResourceVersion", version); - try - { - if (!clientPool.TryAdd(hostname, client)) - { - // As the pool contains the key already, get the HttpClient from the pool. - client = clientPool[hostname]; - } - } - catch (OverflowException) - { - // The dictionary already contains the maximum number of elements(MaxValue) - throw; - } - catch(Exception) + AddResourcesVersionValidation(version, client); + if (!clientPool.TryAdd(hostname, client)) { - throw; + client.Dispose();//we failed adding the new one because of concurrency issues, diposing to avoid memory leaks + client = clientPool[hostname];// As the pool contains the key already, get the HttpClient from the pool. } } // Get Token everytime via ADAL - await Settings.UCWAClient.GetToken(client, uri); + if (!anonymous && client.DefaultRequestHeaders.Authorization == null) + await Settings.UCWAClient.GetToken(client, uri); return client; } + private static void AddResourcesVersionValidation(string version, HttpClient client) + { + client.DefaultRequestHeaders.TryAddWithoutValidation("X-MS-RequiresMinResourceVersion", version); + } } } diff --git a/UCWASDK/UCWASDK/Services/Settings.cs b/UCWASDK/UCWASDK/Services/Settings.cs index 9c8b8e5..5ac5db1 100644 --- a/UCWASDK/UCWASDK/Services/Settings.cs +++ b/UCWASDK/UCWASDK/Services/Settings.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Skype.UCWA.Services +namespace Microsoft.Skype.UCWA.Services { static class Settings { @@ -14,5 +8,6 @@ static class Settings public static string Password { get; set; } public static string ClientId { get; set; } public static UCWAClient UCWAClient { get; set; } + public static bool IsOffice365PublicTenant { get; internal set; } } } diff --git a/UCWASDK/UCWASDK/UCWAClient.cs b/UCWASDK/UCWASDK/UCWAClient.cs index cab8a5c..57797cc 100644 --- a/UCWASDK/UCWASDK/UCWAClient.cs +++ b/UCWASDK/UCWASDK/UCWAClient.cs @@ -662,9 +662,10 @@ public void Dispose() /// /// Office 365 tenant name /// - public async Task Initialize(string tenant) + public async Task Initialize(string tenant, bool isOffice365PublicTenant = true) { Settings.Tenant = tenant; + Settings.IsOffice365PublicTenant = isOffice365PublicTenant; return await CreateApplication(); } @@ -683,22 +684,22 @@ public async Task SignIn(Availability availability, bool supportMessage, bool su bool supportPlainText, bool supportHtmlFormat, string phoneNumber, bool keepAlive) { if (application == null) - throw new Exception("You need to initialize and subscribe the event before starting."); + throw new InvalidOperationException("You need to initialize and subscribe the event before starting."); await application.Me.MakeMeAvailable(availability, supportMessage, supportAudio, supportPlainText, supportHtmlFormat, phoneNumber); application = await application.Get(); if (keepAlive) - KeepAlive(60); + Parallel.Invoke(async () => await KeepAlive(60)); - MonitorEvent(); + Parallel.Invoke(async () => await MonitorEvent()); } /// /// Keep the status by sending ReportActivity. /// /// Interval to send ReportActivity in seconds. Default value is 60. - private async void KeepAlive(int durationInSeconds = 60) + private async Task KeepAlive(int durationInSeconds = 60) { while (Me != null) { @@ -739,7 +740,7 @@ public async Task UnSubscribeContactsChange(params string[] sips) PresenceSubscriptions presenceSubscriptions = await application.People.GetPresenceSubscriptions(); foreach (var sip in sips) { - PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.Where(x => x.Id == sip).FirstOrDefault(); + PresenceSubscription presenceSubscription = presenceSubscriptions.Subscriptions.FirstOrDefault(x => x.Id == sip); if (presenceSubscription != null) await presenceSubscription.Delete(); } @@ -800,8 +801,8 @@ public async Task GetConversation(string subject = "", string titl return null; Conversation conversation = string.IsNullOrEmpty(subject) ? - convs.Where(x => x.Title.ToLower() == title.ToLower()).FirstOrDefault() : - convs.Where(x => x.Subject.ToLower() == subject.ToLower()).FirstOrDefault(); + convs.FirstOrDefault(x => x.Title.ToLower() == title.ToLower()) : + convs.FirstOrDefault(x => x.Subject.ToLower() == subject.ToLower()); return conversation; } @@ -826,7 +827,7 @@ public async Task StartMessaging(string sip, string subject = "", Importance imp /// public async Task StartOnlineMeeting(string subject, Importance importance = Importance.Normal) { - string location = await application.Communication.StartOnlineMeeting(subject, importance); + await application.Communication.StartOnlineMeeting(subject, importance); } /// @@ -971,36 +972,30 @@ public async Task ReplyMessage(string text, OnlineMeetingInvitation onlineMeetin private async Task CreateApplication(string agentName = "myAgent", string language = "en-US") { - try - { - User user = await GetUserDiscoverUri(); - if (user == null) - return false; + User user = await GetUserDiscoverUri(); + if (user == null) + return false; - application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); + application = await user.CreateApplication(agentName, Guid.NewGuid().ToString(), language); - // Get host address - Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; + // Get host address + Settings.Host = new Uri(user.Self).Scheme + "://" + new Uri(user.Self).Host; - eventUri = application.Links.Events; - return true; - } - catch (Exception ex) - { - throw ex; - } + eventUri = application.Links.Events; + return true; } private async Task GetUserDiscoverUri() { - using (HttpClient client = new HttpClient()) + using (var client = new HttpClient()) { + client.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json"); HttpResponseMessage response = null; try { try { - response = await client.GetAsync($"https://lyncdiscoverinternal.{Settings.Tenant}"); + response = await client.GetAsync(Settings.IsOffice365PublicTenant ? $"https://webdir.online.lync.com/Autodiscover/AutodiscoverService.svc/root?originalDomain={Settings.Tenant}" : $"https://lyncdiscoverinternal.{Settings.Tenant}"); } catch { @@ -1025,18 +1020,11 @@ private async Task GetUserDiscoverUri() throw; } } - catch (Exception ex) - { - throw; - } if (response.IsSuccessStatusCode) { var root = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Include }); var redirect = await root.GetRedirect(); - if (redirect != null) - return await redirect.GetUser(); - else - return await root.GetUser(); + return await (redirect?.GetUser() ?? root.GetUser()); } else return null; @@ -1055,14 +1043,8 @@ public async Task Refresh() private async Task GetEvent() { - while (true) - { - if (string.IsNullOrEmpty(eventUri)) - { - return null; - } - using (HttpClient client = new HttpClient()) - { + while (!string.IsNullOrEmpty(eventUri)) + using (var client = new HttpClient()) try { var ucwaEvent = await HttpService.Get(eventUri); @@ -1070,28 +1052,23 @@ private async Task GetEvent() if (ucwaEvent == null) await Task.Delay(1000); - if (!string.IsNullOrEmpty(ucwaEvent.Links.Resync)) + if (!string.IsNullOrEmpty(ucwaEvent?.Links?.Resync)) { eventUri = ucwaEvent.Links.Resync; ucwaEvent = await GetEvent(); } - eventUri = ucwaEvent.Links.Next; + eventUri = ucwaEvent?.Links?.Next; return ucwaEvent; } - catch (TaskCanceledException ex) + catch (TaskCanceledException) { await Task.Delay(1000); } - catch (Exception ex) - { - throw ex; - } - } - } + return null; } - private async void MonitorEvent() + private async Task MonitorEvent() { while (true) {