diff --git a/build.cake b/build.cake index 54218de7d..4ebe73ece 100644 --- a/build.cake +++ b/build.cake @@ -187,7 +187,8 @@ private void SignBinaries(string path) Sign(files, new SignToolSignSettings { ToolPath = MakeAbsolute(File("./certificates/signtool.exe")), - TimeStampUri = new Uri("http://timestamp.globalsign.com/scripts/timestamp.dll"), + TimeStampUri = new Uri("http://rfc3161timestamp.globalsign.com/advanced"), + TimeStampDigestAlgorithm = SignToolDigestAlgorithm.Sha256, CertPath = signingCertificatePath, Password = signingCertificatePassword }); diff --git a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt index 64e9885ae..4d8bb9eca 100644 --- a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt +++ b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETCore.approved.txt @@ -12,8 +12,8 @@ Octopus.Client Octopus.Client.IOctopusClient IDisposable { - event Action AfterReceivingHttpResponse - event Action BeforeSendingHttpRequest + event Action AfterReceivingHttpResponse + event Action BeforeSendingHttpRequest } interface ILinkResolver { @@ -373,8 +373,8 @@ Octopus.Client Octopus.Client.IOctopusClient IDisposable { - event Action AfterReceivingHttpResponse - event Action BeforeSendingHttpRequest + event Action AfterReceivingHttpResponse + event Action BeforeSendingHttpRequest event Action ReceivedOctopusResponse event Action SendingOctopusRequest .ctor(Octopus.Client.OctopusServerEndpoint) diff --git a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt index 17c08aeaf..44ecc9ce6 100644 --- a/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt +++ b/source/Octopus.Client.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress..NETFramework.approved.txt @@ -12,8 +12,8 @@ Octopus.Client Octopus.Client.IOctopusClient IDisposable { - event Action AfterReceivingHttpResponse - event Action BeforeSendingHttpRequest + event Action AfterReceivingHttpResponse + event Action BeforeSendingHttpRequest } interface ILinkResolver { @@ -373,8 +373,8 @@ Octopus.Client Octopus.Client.IOctopusClient IDisposable { - event Action AfterReceivingHttpResponse - event Action BeforeSendingHttpRequest + event Action AfterReceivingHttpResponse + event Action BeforeSendingHttpRequest event Action ReceivedOctopusResponse event Action SendingOctopusRequest .ctor(Octopus.Client.OctopusServerEndpoint) diff --git a/source/Octopus.Client/IHttpOctopusClient.cs b/source/Octopus.Client/IHttpOctopusClient.cs index 734c2e0e1..9f4781480 100644 --- a/source/Octopus.Client/IHttpOctopusClient.cs +++ b/source/Octopus.Client/IHttpOctopusClient.cs @@ -1,5 +1,5 @@ using System; -using System.Net; +using System.Net.Http; namespace Octopus.Client { @@ -11,11 +11,11 @@ public interface IHttpOctopusClient : IOctopusClient /// /// Occurs when a request is about to be sent. /// - event Action BeforeSendingHttpRequest; + event Action BeforeSendingHttpRequest; /// /// Occurs when a response has been received. /// - event Action AfterReceivingHttpResponse; + event Action AfterReceivingHttpResponse; } } diff --git a/source/Octopus.Client/Octopus.Client.csproj b/source/Octopus.Client/Octopus.Client.csproj index 3fc817d41..1f9b9c0bf 100644 --- a/source/Octopus.Client/Octopus.Client.csproj +++ b/source/Octopus.Client/Octopus.Client.csproj @@ -11,6 +11,7 @@ This package contains the client library for the HTTP API in Octopus. Octopus.Client.nuspec + latest net452;netstandard2.0 diff --git a/source/Octopus.Client/OctopusAsyncClient.cs b/source/Octopus.Client/OctopusAsyncClient.cs index 096e01ba0..43e9f43c0 100644 --- a/source/Octopus.Client/OctopusAsyncClient.cs +++ b/source/Octopus.Client/OctopusAsyncClient.cs @@ -526,9 +526,9 @@ protected virtual async Task> DispatchRequest } } - SendingOctopusRequest?.Invoke(request); + OnSendingOctopusRequest(request); - BeforeSendingHttpRequest?.Invoke(message); + OnBeforeSendingHttpRequest(message); if (request.RequestResource != null) message.Content = GetContent(request); @@ -542,7 +542,7 @@ protected virtual async Task> DispatchRequest { using (var response = await client.SendAsync(message, completionOption).ConfigureAwait(false)) { - AfterReceivedHttpResponse?.Invoke(response); + OnAfterReceivedHttpResponse(response); if (!response.IsSuccessStatusCode) throw await OctopusExceptionFactory.CreateException(response).ConfigureAwait(false); @@ -554,7 +554,7 @@ protected virtual async Task> DispatchRequest var locationHeader = response.Headers.Location?.OriginalString; var octopusResponse = new OctopusResponse(request, response.StatusCode, locationHeader, resource); - ReceivedOctopusResponse?.Invoke(octopusResponse); + OnReceivedOctopusResponse(octopusResponse); return octopusResponse; } @@ -566,6 +566,14 @@ protected virtual async Task> DispatchRequest } } + protected virtual void OnSendingOctopusRequest(OctopusRequest request) => SendingOctopusRequest?.Invoke(request); + + protected virtual void OnBeforeSendingHttpRequest(HttpRequestMessage request) => BeforeSendingHttpRequest?.Invoke(request); + + protected virtual void OnAfterReceivedHttpResponse(HttpResponseMessage response) => AfterReceivedHttpResponse?.Invoke(response); + + protected virtual void OnReceivedOctopusResponse(OctopusResponse response) => ReceivedOctopusResponse?.Invoke(response); + private HttpContent GetContent(OctopusRequest request) { var requestStreamContent = request.RequestResource as Stream; diff --git a/source/Octopus.Client/OctopusClient.cs b/source/Octopus.Client/OctopusClient.cs index 05cbe02fa..c3b3e22f7 100644 --- a/source/Octopus.Client/OctopusClient.cs +++ b/source/Octopus.Client/OctopusClient.cs @@ -8,6 +8,9 @@ using Octopus.Client.Serialization; using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; using Octopus.Client.Logging; namespace Octopus.Client @@ -20,10 +23,10 @@ public class OctopusClient : IHttpOctopusClient private static readonly ILog Logger = LogProvider.For(); readonly OctopusServerEndpoint serverEndpoint; + readonly JsonSerializerSettings defaultJsonSerializerSettings = JsonSerialization.GetDefaultSerializerSettings(); + readonly HttpClient client; readonly CookieContainer cookieContainer = new CookieContainer(); readonly Uri cookieOriginUri; - readonly JsonSerializerSettings defaultJsonSerializerSettings = JsonSerialization.GetDefaultSerializerSettings(); - readonly OctopusCustomHeaders octopusCustomHeaders; private string antiforgeryCookieName = null; /// @@ -38,7 +41,21 @@ internal OctopusClient(OctopusServerEndpoint serverEndpoint, string requestingTo { this.serverEndpoint = serverEndpoint; cookieOriginUri = BuildCookieUri(serverEndpoint); - octopusCustomHeaders = new OctopusCustomHeaders(requestingTool); + var handler = new HttpClientHandler() + { + CookieContainer = cookieContainer, + Credentials = serverEndpoint.Credentials ?? CredentialCache.DefaultCredentials + }; + if (serverEndpoint.Proxy != null) + handler.Proxy = serverEndpoint.Proxy; + + client = new HttpClient(handler, true) + { + Timeout = TimeSpan.FromMilliseconds(ApiConstants.DefaultClientRequestTimeout) + }; + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Add(ApiConstants.ApiKeyHttpHeaderName, serverEndpoint.ApiKey); + client.DefaultRequestHeaders.Add("User-Agent", new OctopusCustomHeaders(requestingTool).UserAgent); Repository = new OctopusRepository(this); } @@ -86,12 +103,12 @@ public void SignOut() /// /// Occurs when a request is about to be sent. /// - public event Action BeforeSendingHttpRequest; + public event Action BeforeSendingHttpRequest; /// /// Occurs when a response has been received. /// - public event Action AfterReceivingHttpResponse; + public event Action AfterReceivingHttpResponse; /// /// Occurs when a request is about to be sent. @@ -401,31 +418,16 @@ public Uri QualifyUri(string path, object parameters = null) protected virtual OctopusResponse DispatchRequest(OctopusRequest request, bool readResponse) { - var webRequest = (HttpWebRequest)WebRequest.Create(request.Uri); - if (serverEndpoint.Proxy != null) + using var requestMessage = new HttpRequestMessage { - webRequest.Proxy = serverEndpoint.Proxy; - } - webRequest.CookieContainer = cookieContainer; - webRequest.Accept = "application/json"; - webRequest.ContentType = "application/json"; - webRequest.ReadWriteTimeout = ApiConstants.DefaultClientRequestTimeout; - webRequest.Timeout = ApiConstants.DefaultClientRequestTimeout; - webRequest.Credentials = serverEndpoint.Credentials ?? CredentialCache.DefaultNetworkCredentials; - webRequest.Method = request.Method; - webRequest.Headers[ApiConstants.ApiKeyHttpHeaderName] = serverEndpoint.ApiKey; - webRequest.UserAgent = octopusCustomHeaders.UserAgent; - - if (webRequest.Method == "PUT") - { - webRequest.Headers["X-HTTP-Method-Override"] = "PUT"; - webRequest.Method = "POST"; - } + RequestUri = request.Uri, + Method = new HttpMethod(request.Method) + }; - if (webRequest.Method == "DELETE") + if (request.Method == "PUT" || request.Method == "DELETE") { - webRequest.Headers["X-HTTP-Method-Override"] = "DELETE"; - webRequest.Method = "POST"; + requestMessage.Method = HttpMethod.Post; + requestMessage.Headers.Add("X-HTTP-Method-Override", request.Method); } if (!string.IsNullOrEmpty(antiforgeryCookieName)) @@ -435,148 +437,117 @@ protected virtual OctopusResponse DispatchRequest string.Equals(c.Name, antiforgeryCookieName)); if (antiforgeryCookie != null) { - webRequest.Headers[ApiConstants.AntiforgeryTokenHttpHeaderName] = antiforgeryCookie.Value; + requestMessage.Headers.Add(ApiConstants.AntiforgeryTokenHttpHeaderName, antiforgeryCookie.Value); } } - SendingOctopusRequest?.Invoke(request); + OnSendingOctopusRequest(request); - BeforeSendingHttpRequest?.Invoke(webRequest); + OnBeforeSendingHttpRequest(requestMessage); - HttpWebResponse webResponse = null; + if (request.RequestResource != null) + requestMessage.Content = GetHttpContent(request); + + Logger.Trace($"DispatchRequest: {requestMessage.Method} {requestMessage.RequestUri}"); + + var completionOption = readResponse + ? HttpCompletionOption.ResponseContentRead + : HttpCompletionOption.ResponseHeadersRead; try { - if (request.RequestResource == null) - { - webRequest.ContentLength = 0; - } - else - { - var requestStreamContent = request.RequestResource as Stream; - if (requestStreamContent != null) - { - webRequest.Accept = null; - webRequest.ContentType = "application/octet-stream"; - webRequest.ContentLength = requestStreamContent.Length; - requestStreamContent.CopyTo(webRequest.GetRequestStream()); - // Caller owns stream. - } - else - { - var fileUploadContent = request.RequestResource as FileUpload; - if (fileUploadContent != null) - { - webRequest.AllowWriteStreamBuffering = false; - webRequest.SendChunked = true; - - var boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x"); - var boundarybytes = Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n"); - webRequest.ContentType = "multipart/form-data; boundary=" + boundary; - - var requestStream = webRequest.GetRequestStream(); - requestStream.Write(boundarybytes, 0, boundarybytes.Length); - - var headerTemplate = "Content-Disposition: form-data; filename=\"{0}\"\r\nContent-Type: application/octet-stream\r\n\r\n"; - var header = string.Format(headerTemplate, fileUploadContent.FileName); - var headerbytes = Encoding.UTF8.GetBytes(header); - requestStream.Write(headerbytes, 0, headerbytes.Length); - fileUploadContent.Contents.CopyTo(requestStream); - requestStream.Write(boundarybytes, 0, boundarybytes.Length); - requestStream.Flush(); - requestStream.Close(); - } - else - { - var text = JsonConvert.SerializeObject(request.RequestResource, defaultJsonSerializerSettings); - webRequest.ContentLength = Encoding.UTF8.GetByteCount(text); - var requestStream = new StreamWriter(webRequest.GetRequestStream()); - requestStream.Write(text); - requestStream.Flush(); - } - } - } + using var response = client.SendAsync(requestMessage, completionOption).GetAwaiter().GetResult(); + OnAfterReceivingHttpResponse(response); - Logger.Trace($"DispatchRequest: {webRequest.Method} {webRequest.RequestUri}"); + if (!response.IsSuccessStatusCode) + throw OctopusExceptionFactory.CreateException(response).GetAwaiter().GetResult(); - webResponse = (HttpWebResponse)webRequest.GetResponse(); - AfterReceivingHttpResponse?.Invoke(webResponse); + var resource = readResponse + ? ReadResponse(response) + : default; - var resource = default(TResponseResource); - if (readResponse) - { - var responseStream = webResponse.GetResponseStream(); - if (responseStream != null) - { - if (typeof(TResponseResource) == typeof(Stream)) - { - var stream = new MemoryStream(); - responseStream.CopyTo(stream); - stream.Seek(0, SeekOrigin.Begin); - resource = (TResponseResource)(object)stream; - } - else if (typeof(TResponseResource) == typeof(byte[])) - { - var stream = new MemoryStream(); - responseStream.CopyTo(stream); - resource = (TResponseResource)(object)stream.ToArray(); - } - else if (typeof(TResponseResource) == typeof(string)) - { - using (var reader = new StreamReader(responseStream)) - { - resource = (TResponseResource)(object)reader.ReadToEnd(); - } - } - else - { - using (var reader = new StreamReader(responseStream)) - { - var content = reader.ReadToEnd(); - try - { - resource = JsonConvert.DeserializeObject(content, defaultJsonSerializerSettings); - } - catch (Exception ex) - { - throw new OctopusDeserializationException((int)webResponse.StatusCode, "Unable to process response from server: " + ex.Message + ". Response content: " + (content.Length > 100 ? content.Substring(0, 100) : content), ex); - } - } - } - } - } - - var locationHeader = webResponse.Headers.Get("Location"); - var octopusResponse = new OctopusResponse(request, webResponse.StatusCode, locationHeader, resource); - ReceivedOctopusResponse?.Invoke(octopusResponse); + var locationHeader = response.Headers.Location?.OriginalString; + var octopusResponse = new OctopusResponse(request, response.StatusCode, locationHeader, resource); + OnReceivedOctopusResponse(octopusResponse); return octopusResponse; } - catch (WebException wex) + catch (TaskCanceledException) { - if (wex.Response != null) - { - throw OctopusExceptionFactory.CreateException(wex, (HttpWebResponse)wex.Response); - } + throw new TimeoutException($"Timeout getting response from {request.Uri}, client timeout is set to {client.Timeout}"); + } + } - throw; + private T ReadResponse(HttpResponseMessage response) + { + var content = response.Content; + + if (typeof(T) == typeof(Stream)) + { + var stream = new MemoryStream(); + content.CopyToAsync(stream).GetAwaiter().GetResult(); + stream.Seek(0, SeekOrigin.Begin); + return (T)(object)stream; } - finally + + if (typeof(T) == typeof(byte[])) + return (T) (object) content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + + var str = content.ReadAsStringAsync().GetAwaiter().GetResult(); + if (typeof(T) == typeof(string)) + return (T)(object)str; + + try { - if (webResponse != null) - { - try - { - webResponse.Close(); - } - // ReSharper disable once EmptyGeneralCatchClause - catch - { - } - } + return JsonConvert.DeserializeObject(str, defaultJsonSerializerSettings); + } + catch (Exception ex) + { + throw new OctopusDeserializationException((int)response.StatusCode, + $"Unable to process response from server: {ex.Message}. Response content: {(str.Length > 1000 ? str.Substring(0, 1000) : str)}", ex); } } + private HttpContent GetHttpContent(OctopusRequest request) + { + return request.RequestResource switch + { + Stream requestStream => GetStreamContent(requestStream), + FileUpload fileUpload => GetFileUploadContent(fileUpload), + _ => GetJsonContent(request.RequestResource) + }; + + static HttpContent GetStreamContent(Stream stream) + { + var content = new StreamContent(stream); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + return content; + } + + static HttpContent GetFileUploadContent(FileUpload fileUpload) + { + var formContent = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileUpload.Contents); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + formContent.Add(streamContent, "file", fileUpload.FileName); + return formContent; + } + + HttpContent GetJsonContent(object resource) + { + var text = JsonConvert.SerializeObject(resource, defaultJsonSerializerSettings); + return new StringContent(text, Encoding.UTF8, "application/json"); + } + } + + protected virtual void OnBeforeSendingHttpRequest(HttpRequestMessage request) => BeforeSendingHttpRequest?.Invoke(request); + + protected virtual void OnAfterReceivingHttpResponse(HttpResponseMessage response) => AfterReceivingHttpResponse?.Invoke(response); + + protected virtual void OnSendingOctopusRequest(OctopusRequest request) => SendingOctopusRequest?.Invoke(request); + + protected virtual void OnReceivedOctopusResponse(OctopusResponse response) => ReceivedOctopusResponse?.Invoke(response); + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. ///