From 47002174fdc6435cc2fad5ac542239fdd98cd20a Mon Sep 17 00:00:00 2001 From: juliapaw Date: Fri, 12 May 2023 13:25:14 +0200 Subject: [PATCH 1/8] added HTTP.UploadFile --- .../Frends.Community.PostFile.Tests.cs | 54 +++++ .../Frends.HTTP.UploadFile.Tests.csproj | 27 +++ .../Test_files/test_file.txt | 2 + NewHTTP/Frends.HTTP.UploadFile.sln | 37 ++++ NewHTTP/Frends.HTTP.UploadFile/Definition.cs | 185 +++++++++++++++++ .../Frends.HTTP.UploadFile.cs | 191 ++++++++++++++++++ .../Frends.HTTP.UploadFile.csproj | 28 +++ .../FrendsTaskMetadata.json | 7 + NewHTTP/LICENSE | 21 ++ NewHTTP/README.md | 28 +++ 10 files changed, 580 insertions(+) create mode 100644 NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs create mode 100644 NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj create mode 100644 NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt create mode 100644 NewHTTP/Frends.HTTP.UploadFile.sln create mode 100644 NewHTTP/Frends.HTTP.UploadFile/Definition.cs create mode 100644 NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs create mode 100644 NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj create mode 100644 NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json create mode 100644 NewHTTP/LICENSE create mode 100644 NewHTTP/README.md diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs b/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs new file mode 100644 index 0000000..97dc35c --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs @@ -0,0 +1,54 @@ +using HttpMock; +using HttpMock.Verify.NUnit; +using NUnit.Framework; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile.Tests +{ + [TestFixture] + class TestClass + { + [TestFixture] + public class UnitTest + { + private IHttpServer _stubHttp; + + [SetUp] + public void Setup() + { + _stubHttp = HttpMockRepository.At("http://localhost:9191"); + } + + [Test] + public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() + { + var fileLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); + var codePageName = "iso-8859-1"; + var utf8ByteArray = File.ReadAllBytes(fileLocation); + var expectedContentType = $"text/plain; charset={codePageName}"; + + _stubHttp.Stub(x => x.Post("/endpoint")) + .AsContentType($"text/plain; charset={codePageName}") + .Return("foo åäö") + .OK(); + + var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FileLocation = fileLocation }; + var options = new Options { ConnectionTimeoutSeconds = 60 }; + var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + var requestHead = request.RequestHead; + var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); + var requestContentType = requestHead.Headers["cONTENT-tYpE"]; + + //Casing should not affect setting header. + Assert.That(requestContentType, Is.EqualTo(expectedContentType)); + Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); + } + } + } +} \ No newline at end of file diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj new file mode 100644 index 0000000..840d3b0 --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj @@ -0,0 +1,27 @@ + + + + net471 + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt b/NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt new file mode 100644 index 0000000..863df82 --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt @@ -0,0 +1,2 @@ +Lorem lipsum +Sumpil merol \ No newline at end of file diff --git a/NewHTTP/Frends.HTTP.UploadFile.sln b/NewHTTP/Frends.HTTP.UploadFile.sln new file mode 100644 index 0000000..cbba021 --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile", "Frends.HTTP.UploadFile\Frends.HTTP.UploadFile.csproj", "{35C305C0-8108-4A98-BB1D-AFE5C926239E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile.Tests", "Frends.HTTP.UploadFile.Tests\Frends.HTTP.UploadFile.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78F7F22E-6E20-4BCE-8362-0C558568B729}" + ProjectSection(SolutionItems) = preProject + ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md = ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.Build.0 = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {55BC6629-85C9-48D8-8CA2-B0046AF1AF4B} + EndGlobalSection +EndGlobal diff --git a/NewHTTP/Frends.HTTP.UploadFile/Definition.cs b/NewHTTP/Frends.HTTP.UploadFile/Definition.cs new file mode 100644 index 0000000..71f69f2 --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile/Definition.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.HTTP.UploadFile +{ + /// + /// Represents the HTTP method to be used with the request. + /// + public enum Method + { + /// + /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + /// + POST, + /// + /// The HTTP PUT method is used to replace or update a current resource with new content. + /// + PUT + } + + /// + /// Represents the authentication method to be used with the request. + /// + public enum Authentication + { + /// + /// No authentication is used. + /// + None, + + /// + /// Basic authentication is used, where the username and password are sent in plain text. + /// + Basic, + + /// + /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. + /// + WindowsAuthentication, + + /// + /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. + /// + WindowsIntegratedSecurity, + + /// + /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. + /// + OAuth, + + /// + /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. + /// + ClientCertificate + } + + /// + /// Represents an HTTP header, which consists of a name-value pair. + /// + public class Header + { + /// + /// The name of the header. + /// + public string Name { get; set; } + + /// + /// The value of the header. + /// + public string Value { get; set; } + } + + /// + /// Represents the input data for the HTTP file upload operation. + /// + public class Input + { + /// + /// The HTTP Method to be used with the request. + /// + public Method Method { get; set; } + + /// + /// The URL with protocol and path. You can include query parameters directly in the url. + /// + [DefaultValue("https://example.org/path/to")] + [DisplayFormat(DataFormatString = "Text")] + public string Url { get; set; } + + /// + /// The file location to be posted + /// + public string FileLocation { get; set; } + + /// + /// List of HTTP headers to be added to the request. + /// + public Header[] Headers { get; set; } + } + + /// + /// Options for the HTTP request. + /// + public class Options + { + /// + /// Method of authenticating request + /// + public Authentication Authentication { get; set; } + + /// + /// If WindowsAuthentication is selected you should use domain\username + /// + [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Username { get; set; } + + /// + /// Password for the user. + /// + [PasswordPropertyText] + [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Password { get; set; } + + /// + /// Bearer token to be used for request. Token will be added as Authorization header. + /// + [PasswordPropertyText] + [UIHint(nameof(UploadFile.Authentication), "", Authentication.OAuth)] + public string Token { get; set; } + + /// + /// Thumbprint for using client certificate authentication. + /// + [UIHint(nameof(UploadFile.Authentication), "", Authentication.ClientCertificate)] + public string CertificateThumbprint { get; set; } + + /// + /// Timeout in seconds to be used for the connection and operation. + /// + [DefaultValue(30)] + public int ConnectionTimeoutSeconds { get; set; } + + /// + /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. + /// + [DefaultValue(true)] + public bool FollowRedirects { get; set; } + + /// + /// Do not throw an exception on certificate error. + /// + public bool AllowInvalidCertificate { get; set; } + + /// + /// Some Api's return faulty content-type charset header. This setting overrides the returned charset. + /// + public bool AllowInvalidResponseContentTypeCharSet { get; set; } + /// + /// Throw exception if return code of request is not successfull + /// + public bool ThrowExceptionOnErrorResponse { get; set; } + } + + /// + /// Represents the response received from the HTTP server after sending a request. + /// + public class Response + { + /// + /// The body of the response. + /// + public string Body { get; set; } + + /// + /// The headers of the response, as a dictionary of key-value pairs. + /// + public Dictionary Headers { get; set; } + + /// + /// The status code of the response, as an integer value. + /// + public int StatusCode { get; set; } + } +} \ No newline at end of file diff --git a/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs new file mode 100644 index 0000000..64796ec --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile +{ + /// + /// Represents a task that posts a file to a web API endpoint. + /// + public static class UploadFileTask + { + /// + /// Send file using StreamContent + /// + /// Input parameters + /// Optional parameters with default values + /// The cancellation token that can be used to cancel the upload operation. + /// Object with the following properties: JToken Body. Dictionary(string,string) Headers. int StatusCode + /// public static bool Delete([PropertyTab] string fileName, [PropertyTab] OptionsClass options) + public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) + { + using (var handler = new HttpClientHandler()) + { + handler.SetHandleSettingsBasedOnOptions(options); + + using (var httpClient = new HttpClient(handler)) + { + var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + string body = string.Empty; + IEnumerable>> contentHeaders = new Dictionary>(); + + if (responseMessage.Content != null) + { + body = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + contentHeaders = responseMessage.Content.Headers; + } + var response = new Response + { + Body = body, + StatusCode = (int)responseMessage.StatusCode, + Headers = GetResponseHeaderDictionary((IEnumerable>>)responseMessage.Headers ?? new Dictionary>(), contentHeaders) + }; + + if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse) + { + throw new WebException($"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}"); + } + + return response; + } + } + } + + //Combine response- and responsecontent header to one dictionary + private static Dictionary GetResponseHeaderDictionary(IEnumerable>> responseMessageHeaders, IEnumerable>> contentHeaders) + { + var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + var allHeaders = contentHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value); + return allHeaders; + } + + private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (options.Authentication == Authentication.Basic || options.Authentication == Authentication.OAuth) + { + switch (options.Authentication) + { + case Authentication.Basic: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))); + break; + case Authentication.OAuth: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", + options.Token); + break; + } + } + + //Do not automtically set expect 100-continue response header + httpClient.DefaultRequestHeaders.ExpectContinue = false; + httpClient.Timeout = TimeSpan.FromSeconds(Convert.ToDouble(options.ConnectionTimeoutSeconds)); + + //Ignore case for headers and key comparison + var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase); + + using (MemoryStream reader = new MemoryStream(File.ReadAllBytes(input.FileLocation))) + using (HttpContent content = new StreamContent(reader)) + { + //Clear default headers + content.Headers.Clear(); + foreach (var header in headerDict) + { + var requestHeaderAddedSuccessfully = httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + if (!requestHeaderAddedSuccessfully) + { + //Could not add to request headers try to add to content headers + var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value); + if (!contentHeaderAddedSuccessfully) + { + Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}"); + } + } + + } + + var request = new HttpRequestMessage(new HttpMethod(input.Method.ToString()), new Uri(input.Url)) + { + Content = content + }; + + var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (options.AllowInvalidResponseContentTypeCharSet) + { + response.Content.Headers.ContentType.CharSet = null; + } + return response; + } + } + } + + /// + /// Provides extension methods for various types, allowing for additional functionality to be added to existing types. + /// + public static class Extensions + { + internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) + { + switch (options.Authentication) + { + case Authentication.WindowsIntegratedSecurity: + handler.UseDefaultCredentials = true; + break; + case Authentication.WindowsAuthentication: + var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) + { + throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); + } + handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; + case Authentication.ClientCertificate: + handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); + break; + } + + handler.AllowAutoRedirect = options.FollowRedirects; + + if (options.AllowInvalidCertificate) + { + handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; + } + } + + internal static X509Certificate2 GetCertificate(string thumbprint) + { + thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); + var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadOnly); + var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) + { + throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + } + + return signingCert[0]; + } + finally + { + store.Close(); + } + } + } +} \ No newline at end of file diff --git a/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj b/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj new file mode 100644 index 0000000..c19ab2b --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;net471;net6.0 + HiQ Finland + HiQ Finland + MIT + https://github.com/CommunityHiQ/Frends.HTTP.UploadFile + true + Frends + true + 1.0.1 + + + + + PreserveNewest + + + + + + + + + + + diff --git a/NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json b/NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json new file mode 100644 index 0000000..34dfe4d --- /dev/null +++ b/NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json @@ -0,0 +1,7 @@ +{ + "Tasks": [ + { + "TaskMethod": "Frends.HTTP.UploadFile.UploadFileTask.UploadFile" + } + ] +} \ No newline at end of file diff --git a/NewHTTP/LICENSE b/NewHTTP/LICENSE new file mode 100644 index 0000000..fcf70d9 --- /dev/null +++ b/NewHTTP/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Community HiQ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NewHTTP/README.md b/NewHTTP/README.md new file mode 100644 index 0000000..d5881b5 --- /dev/null +++ b/NewHTTP/README.md @@ -0,0 +1,28 @@ +# Frends.HTTP.UploadFile +Frends Task for Executing a HTTP UploadFile. + +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![Build](https://github.com/FrendsPlatform/Frends.HTTP/actions/workflows/Request_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.HTTP/actions) +![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.HTTP.Request) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.HTTP/Frends.HTTP.Request|main) + +Returns the object of body, headers and statuscode. + +## Installing + +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed +https://www.myget.org/F/frends-tasks/api/v2. + +## Building + +Rebuild the project + +`dotnet build` + +Run tests + +`dotnet test` + +Create a NuGet package + +`dotnet pack --configuration Release` From e3c589bfb6fcabdaa1928b261fe5656b0b72f22d Mon Sep 17 00:00:00 2001 From: juliapaw Date: Fri, 12 May 2023 13:30:18 +0200 Subject: [PATCH 2/8] renamed --- .../Frends.Community.PostFile.Tests.cs | 0 .../Frends.HTTP.UploadFile.Tests.csproj | 0 .../Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt | 0 {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile.sln | 0 .../Frends.HTTP.UploadFile/Definition.cs | 0 .../Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs | 0 .../Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj | 0 .../Frends.HTTP.UploadFile/FrendsTaskMetadata.json | 0 {NewHTTP => Frends.HTTP.UploadFile}/LICENSE | 0 {NewHTTP => Frends.HTTP.UploadFile}/README.md | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile.sln (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile/Definition.cs (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/Frends.HTTP.UploadFile/FrendsTaskMetadata.json (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/LICENSE (100%) rename {NewHTTP => Frends.HTTP.UploadFile}/README.md (100%) diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj diff --git a/NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt diff --git a/NewHTTP/Frends.HTTP.UploadFile.sln b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile.sln rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln diff --git a/NewHTTP/Frends.HTTP.UploadFile/Definition.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile/Definition.cs rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs diff --git a/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs diff --git a/NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj diff --git a/NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json similarity index 100% rename from NewHTTP/Frends.HTTP.UploadFile/FrendsTaskMetadata.json rename to Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json diff --git a/NewHTTP/LICENSE b/Frends.HTTP.UploadFile/LICENSE similarity index 100% rename from NewHTTP/LICENSE rename to Frends.HTTP.UploadFile/LICENSE diff --git a/NewHTTP/README.md b/Frends.HTTP.UploadFile/README.md similarity index 100% rename from NewHTTP/README.md rename to Frends.HTTP.UploadFile/README.md From 4a719f9929bd7d8d77f85a9d3c7ddc5304c161d7 Mon Sep 17 00:00:00 2001 From: Julia <130035826+juliapaw@users.noreply.github.com> Date: Fri, 12 May 2023 13:33:59 +0200 Subject: [PATCH 3/8] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b7753e3..d205711 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Frends tasks for HTTP operations. - [Frends.HTTP.Request](Frends.HTTP.Request/README.md) - [Frends.HTTP.RequestBytes](Frends.HTTP.RequestBytes/README.md) - [Frends.HTTP.DownloadFile](Frends.HTTP.DownloadFile/README.md) +- [Frends.HTTP.UploadFile](Frends.HTTP.UploadFile/README.md) # Contributing When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. @@ -17,4 +18,4 @@ When contributing to this repository, please first discuss the change you wish t 4. Push your work back up to your fork 5. Submit a Pull request so that we can review your changes -NOTE: Be sure to merge the latest from "upstream" before making a pull request! \ No newline at end of file +NOTE: Be sure to merge the latest from "upstream" before making a pull request! From 9639db010ac62dfbcc0e8f9662f7f69d5ef0e306 Mon Sep 17 00:00:00 2001 From: juliapaw Date: Wed, 24 May 2023 12:59:39 +0200 Subject: [PATCH 4/8] tests renamed --- Frends.HTTP.DownloadFile/CHANGELOG.md | 2 +- .../Frends.HTTP.UploadFile.Tests.cs | 54 +++++ .../Frends.HTTP.UploadFile.Tests.csproj | 27 +++ .../Test_files/test_file.txt | 2 + .../Frends.HTTP.UploadFile.sln | 37 ++++ .../Frends.HTTP.UploadFile/Definition.cs | 185 +++++++++++++++++ .../Frends.HTTP.UploadFile.cs | 191 ++++++++++++++++++ .../Frends.HTTP.UploadFile.csproj | 28 +++ .../FrendsTaskMetadata.json | 7 + Frends.HTTP.UploadFile/LICENSE | 21 ++ Frends.HTTP.UploadFile/README.md | 28 +++ 11 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json create mode 100644 Frends.HTTP.UploadFile/LICENSE create mode 100644 Frends.HTTP.UploadFile/README.md diff --git a/Frends.HTTP.DownloadFile/CHANGELOG.md b/Frends.HTTP.DownloadFile/CHANGELOG.md index b3b51ff..12afb9c 100644 --- a/Frends.HTTP.DownloadFile/CHANGELOG.md +++ b/Frends.HTTP.DownloadFile/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## [1.0.0] - 2023-01-27 +## [1.0.0] - 2023-05-12 ### Added - Initial implementation. diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs new file mode 100644 index 0000000..97dc35c --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs @@ -0,0 +1,54 @@ +using HttpMock; +using HttpMock.Verify.NUnit; +using NUnit.Framework; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile.Tests +{ + [TestFixture] + class TestClass + { + [TestFixture] + public class UnitTest + { + private IHttpServer _stubHttp; + + [SetUp] + public void Setup() + { + _stubHttp = HttpMockRepository.At("http://localhost:9191"); + } + + [Test] + public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() + { + var fileLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); + var codePageName = "iso-8859-1"; + var utf8ByteArray = File.ReadAllBytes(fileLocation); + var expectedContentType = $"text/plain; charset={codePageName}"; + + _stubHttp.Stub(x => x.Post("/endpoint")) + .AsContentType($"text/plain; charset={codePageName}") + .Return("foo åäö") + .OK(); + + var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FileLocation = fileLocation }; + var options = new Options { ConnectionTimeoutSeconds = 60 }; + var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + var requestHead = request.RequestHead; + var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); + var requestContentType = requestHead.Headers["cONTENT-tYpE"]; + + //Casing should not affect setting header. + Assert.That(requestContentType, Is.EqualTo(expectedContentType)); + Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); + } + } + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj new file mode 100644 index 0000000..840d3b0 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj @@ -0,0 +1,27 @@ + + + + net471 + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt new file mode 100644 index 0000000..863df82 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Test_files/test_file.txt @@ -0,0 +1,2 @@ +Lorem lipsum +Sumpil merol \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln new file mode 100644 index 0000000..cbba021 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile", "Frends.HTTP.UploadFile\Frends.HTTP.UploadFile.csproj", "{35C305C0-8108-4A98-BB1D-AFE5C926239E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile.Tests", "Frends.HTTP.UploadFile.Tests\Frends.HTTP.UploadFile.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78F7F22E-6E20-4BCE-8362-0C558568B729}" + ProjectSection(SolutionItems) = preProject + ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md = ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.Build.0 = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {55BC6629-85C9-48D8-8CA2-B0046AF1AF4B} + EndGlobalSection +EndGlobal diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs new file mode 100644 index 0000000..71f69f2 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.HTTP.UploadFile +{ + /// + /// Represents the HTTP method to be used with the request. + /// + public enum Method + { + /// + /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + /// + POST, + /// + /// The HTTP PUT method is used to replace or update a current resource with new content. + /// + PUT + } + + /// + /// Represents the authentication method to be used with the request. + /// + public enum Authentication + { + /// + /// No authentication is used. + /// + None, + + /// + /// Basic authentication is used, where the username and password are sent in plain text. + /// + Basic, + + /// + /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. + /// + WindowsAuthentication, + + /// + /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. + /// + WindowsIntegratedSecurity, + + /// + /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. + /// + OAuth, + + /// + /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. + /// + ClientCertificate + } + + /// + /// Represents an HTTP header, which consists of a name-value pair. + /// + public class Header + { + /// + /// The name of the header. + /// + public string Name { get; set; } + + /// + /// The value of the header. + /// + public string Value { get; set; } + } + + /// + /// Represents the input data for the HTTP file upload operation. + /// + public class Input + { + /// + /// The HTTP Method to be used with the request. + /// + public Method Method { get; set; } + + /// + /// The URL with protocol and path. You can include query parameters directly in the url. + /// + [DefaultValue("https://example.org/path/to")] + [DisplayFormat(DataFormatString = "Text")] + public string Url { get; set; } + + /// + /// The file location to be posted + /// + public string FileLocation { get; set; } + + /// + /// List of HTTP headers to be added to the request. + /// + public Header[] Headers { get; set; } + } + + /// + /// Options for the HTTP request. + /// + public class Options + { + /// + /// Method of authenticating request + /// + public Authentication Authentication { get; set; } + + /// + /// If WindowsAuthentication is selected you should use domain\username + /// + [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Username { get; set; } + + /// + /// Password for the user. + /// + [PasswordPropertyText] + [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Password { get; set; } + + /// + /// Bearer token to be used for request. Token will be added as Authorization header. + /// + [PasswordPropertyText] + [UIHint(nameof(UploadFile.Authentication), "", Authentication.OAuth)] + public string Token { get; set; } + + /// + /// Thumbprint for using client certificate authentication. + /// + [UIHint(nameof(UploadFile.Authentication), "", Authentication.ClientCertificate)] + public string CertificateThumbprint { get; set; } + + /// + /// Timeout in seconds to be used for the connection and operation. + /// + [DefaultValue(30)] + public int ConnectionTimeoutSeconds { get; set; } + + /// + /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. + /// + [DefaultValue(true)] + public bool FollowRedirects { get; set; } + + /// + /// Do not throw an exception on certificate error. + /// + public bool AllowInvalidCertificate { get; set; } + + /// + /// Some Api's return faulty content-type charset header. This setting overrides the returned charset. + /// + public bool AllowInvalidResponseContentTypeCharSet { get; set; } + /// + /// Throw exception if return code of request is not successfull + /// + public bool ThrowExceptionOnErrorResponse { get; set; } + } + + /// + /// Represents the response received from the HTTP server after sending a request. + /// + public class Response + { + /// + /// The body of the response. + /// + public string Body { get; set; } + + /// + /// The headers of the response, as a dictionary of key-value pairs. + /// + public Dictionary Headers { get; set; } + + /// + /// The status code of the response, as an integer value. + /// + public int StatusCode { get; set; } + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs new file mode 100644 index 0000000..64796ec --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile +{ + /// + /// Represents a task that posts a file to a web API endpoint. + /// + public static class UploadFileTask + { + /// + /// Send file using StreamContent + /// + /// Input parameters + /// Optional parameters with default values + /// The cancellation token that can be used to cancel the upload operation. + /// Object with the following properties: JToken Body. Dictionary(string,string) Headers. int StatusCode + /// public static bool Delete([PropertyTab] string fileName, [PropertyTab] OptionsClass options) + public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) + { + using (var handler = new HttpClientHandler()) + { + handler.SetHandleSettingsBasedOnOptions(options); + + using (var httpClient = new HttpClient(handler)) + { + var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + string body = string.Empty; + IEnumerable>> contentHeaders = new Dictionary>(); + + if (responseMessage.Content != null) + { + body = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + contentHeaders = responseMessage.Content.Headers; + } + var response = new Response + { + Body = body, + StatusCode = (int)responseMessage.StatusCode, + Headers = GetResponseHeaderDictionary((IEnumerable>>)responseMessage.Headers ?? new Dictionary>(), contentHeaders) + }; + + if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse) + { + throw new WebException($"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}"); + } + + return response; + } + } + } + + //Combine response- and responsecontent header to one dictionary + private static Dictionary GetResponseHeaderDictionary(IEnumerable>> responseMessageHeaders, IEnumerable>> contentHeaders) + { + var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + var allHeaders = contentHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value); + return allHeaders; + } + + private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (options.Authentication == Authentication.Basic || options.Authentication == Authentication.OAuth) + { + switch (options.Authentication) + { + case Authentication.Basic: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))); + break; + case Authentication.OAuth: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", + options.Token); + break; + } + } + + //Do not automtically set expect 100-continue response header + httpClient.DefaultRequestHeaders.ExpectContinue = false; + httpClient.Timeout = TimeSpan.FromSeconds(Convert.ToDouble(options.ConnectionTimeoutSeconds)); + + //Ignore case for headers and key comparison + var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase); + + using (MemoryStream reader = new MemoryStream(File.ReadAllBytes(input.FileLocation))) + using (HttpContent content = new StreamContent(reader)) + { + //Clear default headers + content.Headers.Clear(); + foreach (var header in headerDict) + { + var requestHeaderAddedSuccessfully = httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + if (!requestHeaderAddedSuccessfully) + { + //Could not add to request headers try to add to content headers + var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value); + if (!contentHeaderAddedSuccessfully) + { + Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}"); + } + } + + } + + var request = new HttpRequestMessage(new HttpMethod(input.Method.ToString()), new Uri(input.Url)) + { + Content = content + }; + + var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (options.AllowInvalidResponseContentTypeCharSet) + { + response.Content.Headers.ContentType.CharSet = null; + } + return response; + } + } + } + + /// + /// Provides extension methods for various types, allowing for additional functionality to be added to existing types. + /// + public static class Extensions + { + internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) + { + switch (options.Authentication) + { + case Authentication.WindowsIntegratedSecurity: + handler.UseDefaultCredentials = true; + break; + case Authentication.WindowsAuthentication: + var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) + { + throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); + } + handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; + case Authentication.ClientCertificate: + handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); + break; + } + + handler.AllowAutoRedirect = options.FollowRedirects; + + if (options.AllowInvalidCertificate) + { + handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; + } + } + + internal static X509Certificate2 GetCertificate(string thumbprint) + { + thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); + var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadOnly); + var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) + { + throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + } + + return signingCert[0]; + } + finally + { + store.Close(); + } + } + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj new file mode 100644 index 0000000..c19ab2b --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;net471;net6.0 + HiQ Finland + HiQ Finland + MIT + https://github.com/CommunityHiQ/Frends.HTTP.UploadFile + true + Frends + true + 1.0.1 + + + + + PreserveNewest + + + + + + + + + + + diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json new file mode 100644 index 0000000..34dfe4d --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/FrendsTaskMetadata.json @@ -0,0 +1,7 @@ +{ + "Tasks": [ + { + "TaskMethod": "Frends.HTTP.UploadFile.UploadFileTask.UploadFile" + } + ] +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/LICENSE b/Frends.HTTP.UploadFile/LICENSE new file mode 100644 index 0000000..fcf70d9 --- /dev/null +++ b/Frends.HTTP.UploadFile/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Community HiQ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Frends.HTTP.UploadFile/README.md b/Frends.HTTP.UploadFile/README.md new file mode 100644 index 0000000..d5881b5 --- /dev/null +++ b/Frends.HTTP.UploadFile/README.md @@ -0,0 +1,28 @@ +# Frends.HTTP.UploadFile +Frends Task for Executing a HTTP UploadFile. + +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![Build](https://github.com/FrendsPlatform/Frends.HTTP/actions/workflows/Request_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.HTTP/actions) +![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.HTTP.Request) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.HTTP/Frends.HTTP.Request|main) + +Returns the object of body, headers and statuscode. + +## Installing + +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed +https://www.myget.org/F/frends-tasks/api/v2. + +## Building + +Rebuild the project + +`dotnet build` + +Run tests + +`dotnet test` + +Create a NuGet package + +`dotnet pack --configuration Release` From e80e2def7a59353fc7c69047cb59c1150eee1e1b Mon Sep 17 00:00:00 2001 From: juliapaw Date: Wed, 14 Jun 2023 00:04:02 +0200 Subject: [PATCH 5/8] Update according to comments --- .../Frends.HTTP.UploadFile.Tests.cs | 45 +++ .../Frends.HTTP.UploadFile.Tests.csproj | 10 +- .../Definitions/Authentication.cs | 38 +++ .../Definitions/Header.cs | 27 ++ .../Definitions/Input.cs | 42 +++ .../Definitions/Method.cs | 18 ++ .../Definitions/Options.cs | 79 ++++++ .../Definitions/Response.cs | 43 +++ .../Frends.HTTP.UploadFile.cs | 263 +++++++++--------- .../Frends.HTTP.UploadFile.csproj | 10 +- Frends.HTTP.UploadFile/README.md | 4 +- 11 files changed, 428 insertions(+), 151 deletions(-) create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs index 97dc35c..99a9fd4 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs @@ -1,3 +1,7 @@ +<<<<<<< HEAD +using Frends.HTTP.UploadFile.Definitions; +======= +>>>>>>> 53d927a6357435b7526d4760bf9e519a40c06893 using HttpMock; using HttpMock.Verify.NUnit; using NUnit.Framework; @@ -7,6 +11,46 @@ using System.Threading; using System.Threading.Tasks; +<<<<<<< HEAD +namespace Frends.HTTP.UploadFile.Tests; + +[TestFixture] +public class UnitTest +{ + private IHttpServer _stubHttp; + + [SetUp] + public void Setup() + { + _stubHttp = HttpMockRepository.At("http://localhost:9191"); + } + + [Test] + public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() + { + var filePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); + var codePageName = "iso-8859-1"; + var utf8ByteArray = File.ReadAllBytes(filePath); + var expectedContentType = $"text/plain; charset={codePageName}"; + + _stubHttp.Stub(x => x.Post("/endpoint")) + .AsContentType($"text/plain; charset={codePageName}") + .Return("foo åäö") + .OK(); + + var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FilePath = filePath }; + var options = new Options { ConnectionTimeoutSeconds = 60 }; + var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + var requestHead = request.RequestHead; + var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); + var requestContentType = requestHead.Headers["cONTENT-tYpE"]; + + //Casing should not affect setting header. + Assert.That(requestContentType, Is.EqualTo(expectedContentType)); + Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); +======= namespace Frends.HTTP.UploadFile.Tests { [TestFixture] @@ -50,5 +94,6 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); } } +>>>>>>> 53d927a6357435b7526d4760bf9e519a40c06893 } } \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj index 840d3b0..abae24e 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj @@ -1,7 +1,7 @@  - net471 + net6.0 false @@ -10,13 +10,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs new file mode 100644 index 0000000..5caf198 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs @@ -0,0 +1,38 @@ +namespace Frends.HTTP.UploadFile.Definitions +{ + /// + /// Represents the authentication method to be used with the request. + /// + public enum Authentication + { + /// + /// No authentication is used. + /// + None, + + /// + /// Basic authentication is used, where the username and password are sent in plain text. + /// + Basic, + + /// + /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. + /// + WindowsAuthentication, + + /// + /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. + /// + WindowsIntegratedSecurity, + + /// + /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. + /// + OAuth, + + /// + /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. + /// + ClientCertificate + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs new file mode 100644 index 0000000..5ea7bcc --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile.Definitions +{ + /// + /// Represents an HTTP header, which consists of a name-value pair. + /// + public class Header + { + /// + /// The name of the header. + /// + /// Example Name + public string Name { get; set; } + + /// + /// The value of the header. + /// + /// Example Value + public string Value { get; set; } + + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs new file mode 100644 index 0000000..240d6c1 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile.Definitions +{ + /// + /// Represents the input data for the HTTP file upload operation. + /// + public class Input + { + /// + /// The HTTP Method to be used with the request. + /// + /// GET + public Method Method { get; set; } + + /// + /// The URL with protocol and path. You can include query parameters directly in the url. + /// + /// https://example.org/path/to + [DefaultValue("https://example.org/path/to")] + [DisplayFormat(DataFormatString = "Text")] + public string Url { get; set; } + + /// + /// The file path to be posted + /// + /// C:\Users + public string FilePath { get; set; } + + /// + /// List of HTTP headers to be added to the request. + /// + /// Name: Header, Value: HeaderValue + public Header[] Headers { get; set; } + } +} diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs new file mode 100644 index 0000000..dc9b0a0 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs @@ -0,0 +1,18 @@ +namespace Frends.HTTP.UploadFile.Definitions +{ + + /// + /// Represents the HTTP method to be used with the request. + /// + public enum Method + { + /// + /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + /// + POST, + /// + /// The HTTP PUT method is used to replace or update a current resource with new content. + /// + PUT + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs new file mode 100644 index 0000000..dcc9e97 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; + +namespace Frends.HTTP.UploadFile.Definitions +{ + /// + /// Options for the HTTP request. + /// + public class Options + { + /// + /// Method of authenticating request + /// + /// Basic + public Authentication Authentication { get; set; } + + /// + /// If WindowsAuthentication is selected you should use domain\username + /// + /// Domain\User + [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Username { get; set; } + + /// + /// Password for the user. + /// + /// Example Password + [PasswordPropertyText] + [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Password { get; set; } + + /// + /// Bearer token to be used for request. Token will be added as Authorization header. + /// + /// exampleOAuthToken + [PasswordPropertyText] + [UIHint(nameof(Definitions.Authentication), "", Authentication.OAuth)] + public string Token { get; set; } + + /// + /// Thumbprint for using client certificate authentication. + /// + /// exampleCertificateThumbprint + [UIHint(nameof(Definitions.Authentication), "", Authentication.ClientCertificate)] + public string CertificateThumbprint { get; set; } + + /// + /// Timeout in seconds to be used for the connection and operation. + /// + /// 30 + [DefaultValue(30)] + public int ConnectionTimeoutSeconds { get; set; } + + /// + /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. + /// + /// /// true + [DefaultValue(true)] + public bool FollowRedirects { get; set; } + + /// + /// Do not throw an exception on certificate error. + /// + /// true + public bool AllowInvalidCertificate { get; set; } + + /// + /// Some API's return faulty content-type charset header. This setting overrides the returned charset. + /// + /// false + public bool AllowInvalidResponseContentTypeCharSet { get; set; } + + /// + /// Throw exception if return code of request is not successfull + /// + /// true + public bool ThrowExceptionOnErrorResponse { get; set; } + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs new file mode 100644 index 0000000..5184c17 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Frends.HTTP.UploadFile.Definitions; + + +/// +/// Represents the response received from the HTTP server after sending a request. +/// +public class Response +{ + /// + /// The body of the response. + /// + /// + /// + /// + public string Body { get; set; } + + /// + /// The headers of the response, as a dictionary of key-value pairs. + /// + /// + /// + /// + public Dictionary Headers { get; set; } + + /// + /// The status code of the response, as an integer value. + /// + /// 200 + public int StatusCode { get; set; } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs index 64796ec..4f692ed 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs @@ -13,179 +13,164 @@ using System.Threading; using System.Threading.Tasks; -namespace Frends.HTTP.UploadFile +namespace Frends.HTTP.UploadFile.Definitions; + +/// +/// Represents a task that posts a file to a web API endpoint. +/// +public static class UploadFileTask { /// - /// Represents a task that posts a file to a web API endpoint. + /// Send file using StreamContent, the file data is read from a Stream and sent as the content of an HTTP request. + /// StreamContent is a class provided by the .NET framework that allows you to send content from a Stream in an HTTP request /// - public static class UploadFileTask + /// The input parameters specifying the file to be sent. + /// The optional parameters controlling the file upload behavior. + /// The cancellation token that can be used to cancel the upload operation. + /// An object containing the response from the server, including the response body, headers, and status code. + public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) { - /// - /// Send file using StreamContent - /// - /// Input parameters - /// Optional parameters with default values - /// The cancellation token that can be used to cancel the upload operation. - /// Object with the following properties: JToken Body. Dictionary(string,string) Headers. int StatusCode - /// public static bool Delete([PropertyTab] string fileName, [PropertyTab] OptionsClass options) - public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) - { - using (var handler = new HttpClientHandler()) - { - handler.SetHandleSettingsBasedOnOptions(options); + using var handler = new HttpClientHandler(); + handler.SetHandleSettingsBasedOnOptions(options); - using (var httpClient = new HttpClient(handler)) - { - var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - - string body = string.Empty; - IEnumerable>> contentHeaders = new Dictionary>(); - - if (responseMessage.Content != null) - { - body = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); - contentHeaders = responseMessage.Content.Headers; - } - var response = new Response - { - Body = body, - StatusCode = (int)responseMessage.StatusCode, - Headers = GetResponseHeaderDictionary((IEnumerable>>)responseMessage.Headers ?? new Dictionary>(), contentHeaders) - }; - - if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse) - { - throw new WebException($"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}"); - } - - return response; - } - } - } + using var httpClient = new HttpClient(handler); + var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); - //Combine response- and responsecontent header to one dictionary - private static Dictionary GetResponseHeaderDictionary(IEnumerable>> responseMessageHeaders, IEnumerable>> contentHeaders) - { - var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); - var allHeaders = contentHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); - responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value); - return allHeaders; - } + string body = string.Empty; + IEnumerable>> contentHeaders = new Dictionary>(); - private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken) + if (responseMessage.Content is not null) { - cancellationToken.ThrowIfCancellationRequested(); - if (options.Authentication == Authentication.Basic || options.Authentication == Authentication.OAuth) - { - switch (options.Authentication) - { - case Authentication.Basic: - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", - Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))); - break; - case Authentication.OAuth: - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", - options.Token); - break; - } - } - - //Do not automtically set expect 100-continue response header - httpClient.DefaultRequestHeaders.ExpectContinue = false; - httpClient.Timeout = TimeSpan.FromSeconds(Convert.ToDouble(options.ConnectionTimeoutSeconds)); + body = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + contentHeaders = responseMessage.Content.Headers; + } - //Ignore case for headers and key comparison - var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase); + var headers = ((IEnumerable>>) responseMessage.Headers ?? new Dictionary>()); + var responseHeaders = GetResponseHeaderDictionary(headers, contentHeaders); - using (MemoryStream reader = new MemoryStream(File.ReadAllBytes(input.FileLocation))) - using (HttpContent content = new StreamContent(reader)) - { - //Clear default headers - content.Headers.Clear(); - foreach (var header in headerDict) - { - var requestHeaderAddedSuccessfully = httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); - if (!requestHeaderAddedSuccessfully) - { - //Could not add to request headers try to add to content headers - var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value); - if (!contentHeaderAddedSuccessfully) - { - Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}"); - } - } - - } + var response = new Response + { + Body = body, + StatusCode = (int)responseMessage.StatusCode, + Headers = responseHeaders + }; - var request = new HttpRequestMessage(new HttpMethod(input.Method.ToString()), new Uri(input.Url)) - { - Content = content - }; + if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse) + { + throw new WebException($"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}"); + } - var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + return response; + } - if (options.AllowInvalidResponseContentTypeCharSet) - { - response.Content.Headers.ContentType.CharSet = null; - } - return response; - } - } + //Combine response- and responsecontent header to one dictionary + private static Dictionary GetResponseHeaderDictionary(IEnumerable>> responseMessageHeaders, IEnumerable>> contentHeaders) + { + var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + var allHeaders = contentHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); + responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value); + return allHeaders; } - /// - /// Provides extension methods for various types, allowing for additional functionality to be added to existing types. - /// - public static class Extensions + private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken) { - internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) + cancellationToken.ThrowIfCancellationRequested(); + if (options.Authentication == Authentication.Basic || options.Authentication == Authentication.OAuth) { switch (options.Authentication) { - case Authentication.WindowsIntegratedSecurity: - handler.UseDefaultCredentials = true; + case Authentication.Basic: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))); break; - case Authentication.WindowsAuthentication: - var domainAndUserName = options.Username.Split('\\'); - if (domainAndUserName.Length != 2) - { - throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); - } - handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); - break; - case Authentication.ClientCertificate: - handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); + case Authentication.OAuth: + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", + options.Token); break; } + } - handler.AllowAutoRedirect = options.FollowRedirects; + using MemoryStream reader = new MemoryStream(File.ReadAllBytes(input.FilePath)); + using HttpContent content = new StreamContent(reader); + var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase); - if (options.AllowInvalidCertificate) + foreach (var header in headerDict) + { + var requestHeaderAddedSuccessfully = httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + if (!requestHeaderAddedSuccessfully) { - handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; + // Could not add to request headers, try to add to content headers + var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value); + if (!contentHeaderAddedSuccessfully) + { + Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}"); + } } } - internal static X509Certificate2 GetCertificate(string thumbprint) + var request = new HttpRequestMessage(new HttpMethod(input.Method.ToString()), new Uri(input.Url)) { - thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - try - { - store.Open(OpenFlags.ReadOnly); - var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); - if (signingCert.Count == 0) + Content = content + }; + + var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + return response; + } +} + +/// +/// Provides extension methods for various types, allowing for additional functionality to be added to existing types. +/// +internal static class HttpClientHandlerExtensions +{ + internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) + { + switch (options.Authentication) + { + case Authentication.WindowsIntegratedSecurity: + handler.UseDefaultCredentials = true; + break; + case Authentication.WindowsAuthentication: + var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) { - throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); } + handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; + case Authentication.ClientCertificate: + handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); + break; + } - return signingCert[0]; - } - finally + handler.AllowAutoRedirect = options.FollowRedirects; + + if (options.AllowInvalidCertificate) + { + handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; + } + } + + internal static X509Certificate2 GetCertificate(string thumbprint) + { + thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); + var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadOnly); + var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) { - store.Close(); + throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); } + + return signingCert[0]; + } + finally + { + store.Close(); } } } \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj index c19ab2b..a64fa71 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.csproj @@ -1,15 +1,15 @@  - netstandard2.0;net471;net6.0 - HiQ Finland - HiQ Finland + net6.0 + Frends + Frends MIT - https://github.com/CommunityHiQ/Frends.HTTP.UploadFile + https://github.com/FrendsPlatform/Frends.HTTP true Frends true - 1.0.1 + 1.0.0 diff --git a/Frends.HTTP.UploadFile/README.md b/Frends.HTTP.UploadFile/README.md index d5881b5..e065fa8 100644 --- a/Frends.HTTP.UploadFile/README.md +++ b/Frends.HTTP.UploadFile/README.md @@ -3,8 +3,8 @@ Frends Task for Executing a HTTP UploadFile. [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) [![Build](https://github.com/FrendsPlatform/Frends.HTTP/actions/workflows/Request_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.HTTP/actions) -![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.HTTP.Request) -![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.HTTP/Frends.HTTP.Request|main) +![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.HTTP/Frends.HTTP.UploadFile) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.HTTP/Frends.HTTP.UploadFile|main) Returns the object of body, headers and statuscode. From 374f7fe6ebeef4f5c74c85d385320073357960de Mon Sep 17 00:00:00 2001 From: "mateusz.wojtania" Date: Fri, 10 Jan 2025 08:20:14 +0100 Subject: [PATCH 6/8] refactori --- .../UploadFile_build_and_test_on_main.yml | 17 ++ .../UploadFile_build_and_test_on_push.yml | 18 ++ .github/workflows/UploadFile_release.yml | 12 ++ Frends.HTTP.UploadFile/CHANGELOG.md | 5 + .../Frends.Community.PostFile.Tests.cs | 54 ----- .../Frends.HTTP.UploadFile.Tests.cs | 99 ---------- .../IntegrationTest.cs | 50 +++++ .../Frends.HTTP.UploadFile.sln | 4 +- .../Frends.HTTP.UploadFile/Definition.cs | 185 ------------------ .../Definitions/Authentication.cs | 59 +++--- .../Definitions/Header.cs | 36 ++-- .../Definitions/Input.cs | 61 +++--- .../Definitions/Method.cs | 29 ++- .../Definitions/Options.cs | 127 ++++++------ .../Definitions/Response.cs | 1 - .../Frends.HTTP.UploadFile.cs | 92 ++------- .../HttpClientHandlerExtensions.cs | 64 ++++++ Frends.HTTP.UploadFile/LICENSE | 21 -- 18 files changed, 331 insertions(+), 603 deletions(-) create mode 100644 .github/workflows/UploadFile_build_and_test_on_main.yml create mode 100644 .github/workflows/UploadFile_build_and_test_on_push.yml create mode 100644 .github/workflows/UploadFile_release.yml create mode 100644 Frends.HTTP.UploadFile/CHANGELOG.md delete mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs delete mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs delete mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs delete mode 100644 Frends.HTTP.UploadFile/LICENSE diff --git a/.github/workflows/UploadFile_build_and_test_on_main.yml b/.github/workflows/UploadFile_build_and_test_on_main.yml new file mode 100644 index 0000000..3e5b6c8 --- /dev/null +++ b/.github/workflows/UploadFile_build_and_test_on_main.yml @@ -0,0 +1,17 @@ +name: UploadFile build_main + +on: + push: + branches: + - main + paths: + - "Frends.HTTP.UploadFile/**" + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main + with: + workdir: Frends.HTTP.UploadFile + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} diff --git a/.github/workflows/UploadFile_build_and_test_on_push.yml b/.github/workflows/UploadFile_build_and_test_on_push.yml new file mode 100644 index 0000000..0e4cbf3 --- /dev/null +++ b/.github/workflows/UploadFile_build_and_test_on_push.yml @@ -0,0 +1,18 @@ +name: UploadFile push + +on: + push: + branches-ignore: + - main + paths: + - "Frends.HTTP.UploadFile/**" + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main + with: + workdir: Frends.HTTP.UploadFile + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} + test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }} diff --git a/.github/workflows/UploadFile_release.yml b/.github/workflows/UploadFile_release.yml new file mode 100644 index 0000000..2efb48b --- /dev/null +++ b/.github/workflows/UploadFile_release.yml @@ -0,0 +1,12 @@ +name: UploadFile release + +on: + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main + with: + workdir: Frends.HTTP.UploadFile + secrets: + feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }} diff --git a/Frends.HTTP.UploadFile/CHANGELOG.md b/Frends.HTTP.UploadFile/CHANGELOG.md new file mode 100644 index 0000000..17139d1 --- /dev/null +++ b/Frends.HTTP.UploadFile/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [1.0.0] - 2025-01-09 +### Added +- Initial implementation. diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs deleted file mode 100644 index 97dc35c..0000000 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.Community.PostFile.Tests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using HttpMock; -using HttpMock.Verify.NUnit; -using NUnit.Framework; -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Frends.HTTP.UploadFile.Tests -{ - [TestFixture] - class TestClass - { - [TestFixture] - public class UnitTest - { - private IHttpServer _stubHttp; - - [SetUp] - public void Setup() - { - _stubHttp = HttpMockRepository.At("http://localhost:9191"); - } - - [Test] - public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() - { - var fileLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); - var codePageName = "iso-8859-1"; - var utf8ByteArray = File.ReadAllBytes(fileLocation); - var expectedContentType = $"text/plain; charset={codePageName}"; - - _stubHttp.Stub(x => x.Post("/endpoint")) - .AsContentType($"text/plain; charset={codePageName}") - .Return("foo åäö") - .OK(); - - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FileLocation = fileLocation }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; - var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); - var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); - var requestHead = request.RequestHead; - var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); - var requestContentType = requestHead.Headers["cONTENT-tYpE"]; - - //Casing should not affect setting header. - Assert.That(requestContentType, Is.EqualTo(expectedContentType)); - Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); - } - } - } -} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs deleted file mode 100644 index 99a9fd4..0000000 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.cs +++ /dev/null @@ -1,99 +0,0 @@ -<<<<<<< HEAD -using Frends.HTTP.UploadFile.Definitions; -======= ->>>>>>> 53d927a6357435b7526d4760bf9e519a40c06893 -using HttpMock; -using HttpMock.Verify.NUnit; -using NUnit.Framework; -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -<<<<<<< HEAD -namespace Frends.HTTP.UploadFile.Tests; - -[TestFixture] -public class UnitTest -{ - private IHttpServer _stubHttp; - - [SetUp] - public void Setup() - { - _stubHttp = HttpMockRepository.At("http://localhost:9191"); - } - - [Test] - public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() - { - var filePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); - var codePageName = "iso-8859-1"; - var utf8ByteArray = File.ReadAllBytes(filePath); - var expectedContentType = $"text/plain; charset={codePageName}"; - - _stubHttp.Stub(x => x.Post("/endpoint")) - .AsContentType($"text/plain; charset={codePageName}") - .Return("foo åäö") - .OK(); - - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FilePath = filePath }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; - var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); - var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); - var requestHead = request.RequestHead; - var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); - var requestContentType = requestHead.Headers["cONTENT-tYpE"]; - - //Casing should not affect setting header. - Assert.That(requestContentType, Is.EqualTo(expectedContentType)); - Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); -======= -namespace Frends.HTTP.UploadFile.Tests -{ - [TestFixture] - class TestClass - { - [TestFixture] - public class UnitTest - { - private IHttpServer _stubHttp; - - [SetUp] - public void Setup() - { - _stubHttp = HttpMockRepository.At("http://localhost:9191"); - } - - [Test] - public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() - { - var fileLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); - var codePageName = "iso-8859-1"; - var utf8ByteArray = File.ReadAllBytes(fileLocation); - var expectedContentType = $"text/plain; charset={codePageName}"; - - _stubHttp.Stub(x => x.Post("/endpoint")) - .AsContentType($"text/plain; charset={codePageName}") - .Return("foo åäö") - .OK(); - - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FileLocation = fileLocation }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; - var result = (Response)await UploadFileTask.UploadFile(input, options, CancellationToken.None); - var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); - var requestHead = request.RequestHead; - var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); - var requestContentType = requestHead.Headers["cONTENT-tYpE"]; - - //Casing should not affect setting header. - Assert.That(requestContentType, Is.EqualTo(expectedContentType)); - Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); - } - } ->>>>>>> 53d927a6357435b7526d4760bf9e519a40c06893 - } -} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs new file mode 100644 index 0000000..85be473 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs @@ -0,0 +1,50 @@ +using Frends.HTTP.UploadFile.Definitions; +using HttpMock; +using HttpMock.Verify.NUnit; +using NUnit.Framework; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.HTTP.UploadFile.Tests; + +[TestFixture] +public class IntegrationTest +{ + private IHttpServer _stubHttp; + + [SetUp] + public void Setup() + { + _stubHttp = HttpMockRepository.At("http://localhost:9191"); + } + + [Test] + public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() + { + var filePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); + var codePageName = "iso-8859-1"; + var utf8ByteArray = File.ReadAllBytes(filePath); + var expectedContentType = $"text/plain; charset={codePageName}"; + + _stubHttp.Stub(x => x.Post("/endpoint")) + .AsContentType($"text/plain; charset={codePageName}") + .Return("foo ���") + .OK(); + + var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FilePath = filePath }; + var options = new Options { ConnectionTimeoutSeconds = 60 }; + var result = await UploadFileTask.UploadFile(input, options, CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + var requestHead = request.RequestHead; + var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); + var requestContentType = requestHead.Headers["cONTENT-tYpE"]; + + //Casing should not affect setting header. + Assert.That(requestContentType, Is.EqualTo(expectedContentType)); + Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln index cbba021..8cbfa93 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln @@ -7,9 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile", "F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile.Tests", "Frends.HTTP.UploadFile.Tests\Frends.HTTP.UploadFile.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78F7F22E-6E20-4BCE-8362-0C558568B729}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{500C38D9-EBDF-49FE-ACFB-D22A5191B8BB}" ProjectSection(SolutionItems) = preProject - ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md = ..\Frends.HTTP\Frends.HTTP.DownloadFile\CHANGELOG.md + CHANGELOG.md = CHANGELOG.md README.md = README.md EndProjectSection EndProject diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs deleted file mode 100644 index 71f69f2..0000000 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definition.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; - -namespace Frends.HTTP.UploadFile -{ - /// - /// Represents the HTTP method to be used with the request. - /// - public enum Method - { - /// - /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. - /// - POST, - /// - /// The HTTP PUT method is used to replace or update a current resource with new content. - /// - PUT - } - - /// - /// Represents the authentication method to be used with the request. - /// - public enum Authentication - { - /// - /// No authentication is used. - /// - None, - - /// - /// Basic authentication is used, where the username and password are sent in plain text. - /// - Basic, - - /// - /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. - /// - WindowsAuthentication, - - /// - /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. - /// - WindowsIntegratedSecurity, - - /// - /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. - /// - OAuth, - - /// - /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. - /// - ClientCertificate - } - - /// - /// Represents an HTTP header, which consists of a name-value pair. - /// - public class Header - { - /// - /// The name of the header. - /// - public string Name { get; set; } - - /// - /// The value of the header. - /// - public string Value { get; set; } - } - - /// - /// Represents the input data for the HTTP file upload operation. - /// - public class Input - { - /// - /// The HTTP Method to be used with the request. - /// - public Method Method { get; set; } - - /// - /// The URL with protocol and path. You can include query parameters directly in the url. - /// - [DefaultValue("https://example.org/path/to")] - [DisplayFormat(DataFormatString = "Text")] - public string Url { get; set; } - - /// - /// The file location to be posted - /// - public string FileLocation { get; set; } - - /// - /// List of HTTP headers to be added to the request. - /// - public Header[] Headers { get; set; } - } - - /// - /// Options for the HTTP request. - /// - public class Options - { - /// - /// Method of authenticating request - /// - public Authentication Authentication { get; set; } - - /// - /// If WindowsAuthentication is selected you should use domain\username - /// - [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] - public string Username { get; set; } - - /// - /// Password for the user. - /// - [PasswordPropertyText] - [UIHint(nameof(UploadFile.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] - public string Password { get; set; } - - /// - /// Bearer token to be used for request. Token will be added as Authorization header. - /// - [PasswordPropertyText] - [UIHint(nameof(UploadFile.Authentication), "", Authentication.OAuth)] - public string Token { get; set; } - - /// - /// Thumbprint for using client certificate authentication. - /// - [UIHint(nameof(UploadFile.Authentication), "", Authentication.ClientCertificate)] - public string CertificateThumbprint { get; set; } - - /// - /// Timeout in seconds to be used for the connection and operation. - /// - [DefaultValue(30)] - public int ConnectionTimeoutSeconds { get; set; } - - /// - /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. - /// - [DefaultValue(true)] - public bool FollowRedirects { get; set; } - - /// - /// Do not throw an exception on certificate error. - /// - public bool AllowInvalidCertificate { get; set; } - - /// - /// Some Api's return faulty content-type charset header. This setting overrides the returned charset. - /// - public bool AllowInvalidResponseContentTypeCharSet { get; set; } - /// - /// Throw exception if return code of request is not successfull - /// - public bool ThrowExceptionOnErrorResponse { get; set; } - } - - /// - /// Represents the response received from the HTTP server after sending a request. - /// - public class Response - { - /// - /// The body of the response. - /// - public string Body { get; set; } - - /// - /// The headers of the response, as a dictionary of key-value pairs. - /// - public Dictionary Headers { get; set; } - - /// - /// The status code of the response, as an integer value. - /// - public int StatusCode { get; set; } - } -} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs index 5caf198..0999dc4 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Authentication.cs @@ -1,38 +1,37 @@ -namespace Frends.HTTP.UploadFile.Definitions +namespace Frends.HTTP.UploadFile.Definitions; + +/// +/// Represents the authentication method to be used with the request. +/// +public enum Authentication { /// - /// Represents the authentication method to be used with the request. + /// No authentication is used. /// - public enum Authentication - { - /// - /// No authentication is used. - /// - None, + None, - /// - /// Basic authentication is used, where the username and password are sent in plain text. - /// - Basic, + /// + /// Basic authentication is used, where the username and password are sent in plain text. + /// + Basic, - /// - /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. - /// - WindowsAuthentication, + /// + /// Windows authentication is used, where the user's Windows login credentials are used to authenticate the request. + /// + WindowsAuthentication, - /// - /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. - /// - WindowsIntegratedSecurity, + /// + /// Windows integrated security is used, where the current Windows user's credentials are used to authenticate the request. + /// + WindowsIntegratedSecurity, - /// - /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. - /// - OAuth, + /// + /// OAuth token-based authentication is used, where a token is obtained from an authentication server and used to authenticate the request. + /// + OAuth, - /// - /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. - /// - ClientCertificate - } -} \ No newline at end of file + /// + /// Client certificate-based authentication is used, where a client certificate is used to authenticate the request. + /// + ClientCertificate +} diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs index 5ea7bcc..513a54d 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Header.cs @@ -1,27 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Frends.HTTP.UploadFile.Definitions +namespace Frends.HTTP.UploadFile.Definitions; +/// +/// Represents an HTTP header, which consists of a name-value pair. +/// +public class Header { /// - /// Represents an HTTP header, which consists of a name-value pair. + /// The name of the header. /// - public class Header - { - /// - /// The name of the header. - /// - /// Example Name - public string Name { get; set; } + /// Example Name + public string Name { get; set; } - /// - /// The value of the header. - /// - /// Example Value - public string Value { get; set; } + /// + /// The value of the header. + /// + /// Example Value + public string Value { get; set; } - } -} \ No newline at end of file +} diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs index 240d6c1..d39db1e 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Input.cs @@ -1,42 +1,37 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Frends.HTTP.UploadFile.Definitions +namespace Frends.HTTP.UploadFile.Definitions; + +/// +/// Represents the input data for the HTTP file upload operation. +/// +public class Input { /// - /// Represents the input data for the HTTP file upload operation. + /// The HTTP Method to be used with the request. /// - public class Input - { - /// - /// The HTTP Method to be used with the request. - /// - /// GET - public Method Method { get; set; } + /// GET + public Method Method { get; set; } - /// - /// The URL with protocol and path. You can include query parameters directly in the url. - /// - /// https://example.org/path/to - [DefaultValue("https://example.org/path/to")] - [DisplayFormat(DataFormatString = "Text")] - public string Url { get; set; } + /// + /// The URL with protocol and path. You can include query parameters directly in the url. + /// + /// https://example.org/path/to + [DefaultValue("https://example.org/path/to")] + [DisplayFormat(DataFormatString = "Text")] + public string Url { get; set; } - /// - /// The file path to be posted - /// - /// C:\Users - public string FilePath { get; set; } + /// + /// The file path to be posted + /// + /// C:\Users + public string FilePath { get; set; } - /// - /// List of HTTP headers to be added to the request. - /// - /// Name: Header, Value: HeaderValue - public Header[] Headers { get; set; } - } + /// + /// List of HTTP headers to be added to the request. + /// + /// Name: Header, Value: HeaderValue + public Header[] Headers { get; set; } } + diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs index dc9b0a0..b723e56 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Method.cs @@ -1,18 +1,17 @@ -namespace Frends.HTTP.UploadFile.Definitions -{ +namespace Frends.HTTP.UploadFile.Definitions; + +/// +/// Represents the HTTP method to be used with the request. +/// +public enum Method +{ + /// + /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + /// + POST, /// - /// Represents the HTTP method to be used with the request. + /// The HTTP PUT method is used to replace or update a current resource with new content. /// - public enum Method - { - /// - /// The HTTP POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. - /// - POST, - /// - /// The HTTP PUT method is used to replace or update a current resource with new content. - /// - PUT - } -} \ No newline at end of file + PUT +} diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs index dcc9e97..1251ae4 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Options.cs @@ -1,79 +1,78 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel; -namespace Frends.HTTP.UploadFile.Definitions +namespace Frends.HTTP.UploadFile.Definitions; + +/// +/// Options for the HTTP request. +/// +public class Options { /// - /// Options for the HTTP request. + /// Method of authenticating request /// - public class Options - { - /// - /// Method of authenticating request - /// - /// Basic - public Authentication Authentication { get; set; } + /// Basic + public Authentication Authentication { get; set; } - /// - /// If WindowsAuthentication is selected you should use domain\username - /// - /// Domain\User - [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] - public string Username { get; set; } + /// + /// If WindowsAuthentication is selected you should use domain\username + /// + /// Domain\User + [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Username { get; set; } - /// - /// Password for the user. - /// - /// Example Password - [PasswordPropertyText] - [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] - public string Password { get; set; } + /// + /// Password for the user. + /// + /// Example Password + [PasswordPropertyText] + [UIHint(nameof(Definitions.Authentication), "", Authentication.WindowsAuthentication, Authentication.Basic)] + public string Password { get; set; } - /// - /// Bearer token to be used for request. Token will be added as Authorization header. - /// - /// exampleOAuthToken - [PasswordPropertyText] - [UIHint(nameof(Definitions.Authentication), "", Authentication.OAuth)] - public string Token { get; set; } + /// + /// Bearer token to be used for request. Token will be added as Authorization header. + /// + /// exampleOAuthToken + [PasswordPropertyText] + [UIHint(nameof(Definitions.Authentication), "", Authentication.OAuth)] + public string Token { get; set; } - /// - /// Thumbprint for using client certificate authentication. - /// - /// exampleCertificateThumbprint - [UIHint(nameof(Definitions.Authentication), "", Authentication.ClientCertificate)] - public string CertificateThumbprint { get; set; } + /// + /// Thumbprint for using client certificate authentication. + /// + /// exampleCertificateThumbprint + [UIHint(nameof(Definitions.Authentication), "", Authentication.ClientCertificate)] + public string CertificateThumbprint { get; set; } - /// - /// Timeout in seconds to be used for the connection and operation. - /// - /// 30 - [DefaultValue(30)] - public int ConnectionTimeoutSeconds { get; set; } + /// + /// Timeout in seconds to be used for the connection and operation. + /// + /// 30 + [DefaultValue(30)] + public int ConnectionTimeoutSeconds { get; set; } - /// - /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. - /// - /// /// true - [DefaultValue(true)] - public bool FollowRedirects { get; set; } + /// + /// If FollowRedirects is set to false, all responses with an HTTP status code from 300 to 399 is returned to the application. + /// + /// true + [DefaultValue(true)] + public bool FollowRedirects { get; set; } - /// - /// Do not throw an exception on certificate error. - /// - /// true - public bool AllowInvalidCertificate { get; set; } + /// + /// Do not throw an exception on certificate error. + /// + /// true + public bool AllowInvalidCertificate { get; set; } - /// - /// Some API's return faulty content-type charset header. This setting overrides the returned charset. - /// - /// false - public bool AllowInvalidResponseContentTypeCharSet { get; set; } + /// + /// Some API's return faulty content-type charset header. This setting overrides the returned charset. + /// + /// false + public bool AllowInvalidResponseContentTypeCharSet { get; set; } - /// - /// Throw exception if return code of request is not successfull - /// - /// true - public bool ThrowExceptionOnErrorResponse { get; set; } - } -} \ No newline at end of file + /// + /// Throw exception if return code of request is not successfull + /// + /// true + public bool ThrowExceptionOnErrorResponse { get; set; } +} diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs index 5184c17..bb38da6 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Definitions/Response.cs @@ -2,7 +2,6 @@ namespace Frends.HTTP.UploadFile.Definitions; - /// /// Represents the response received from the HTTP server after sending a request. /// diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs index 4f692ed..b1221db 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.cs @@ -7,13 +7,12 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Frends.HTTP.UploadFile.Definitions; -namespace Frends.HTTP.UploadFile.Definitions; +namespace Frends.HTTP.UploadFile; /// /// Represents a task that posts a file to a web API endpoint. @@ -27,8 +26,8 @@ public static class UploadFileTask /// The input parameters specifying the file to be sent. /// The optional parameters controlling the file upload behavior. /// The cancellation token that can be used to cancel the upload operation. - /// An object containing the response from the server, including the response body, headers, and status code. - public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) + /// Object { string Body, Dictionary[string, string] Headers, int StatusCode } + public static async Task UploadFile([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) { using var handler = new HttpClientHandler(); handler.SetHandleSettingsBasedOnOptions(options); @@ -37,17 +36,11 @@ public static async Task UploadFile([PropertyTab] Input input, [Property var responseMessage = await GetHttpRequestResponseAsync(httpClient, input, options, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - string body = string.Empty; - IEnumerable>> contentHeaders = new Dictionary>(); + string body = await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + HttpHeaders contentHeaders = responseMessage.Content.Headers; + HttpHeaders headers = responseMessage.Headers; - if (responseMessage.Content is not null) - { - body = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); - contentHeaders = responseMessage.Content.Headers; - } - - var headers = ((IEnumerable>>) responseMessage.Headers ?? new Dictionary>()); - var responseHeaders = GetResponseHeaderDictionary(headers, contentHeaders); + var responseHeaders = CombineHeaders(new HttpHeaders[] { headers, contentHeaders }); var response = new Response { @@ -64,13 +57,13 @@ public static async Task UploadFile([PropertyTab] Input input, [Property return response; } - //Combine response- and responsecontent header to one dictionary - private static Dictionary GetResponseHeaderDictionary(IEnumerable>> responseMessageHeaders, IEnumerable>> contentHeaders) + //Combine http headears collections into one dictionary + private static Dictionary CombineHeaders(HttpHeaders[] headers) { - var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); - var allHeaders = contentHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value)); - responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value); - return allHeaders; + var result = headers + .SelectMany(dict => dict) + .ToDictionary(pair => pair.Key, pair => string.Join(";", pair.Value)); + return result; } private static async Task GetHttpRequestResponseAsync(HttpClient httpClient, Input input, Options options, CancellationToken cancellationToken) @@ -91,7 +84,7 @@ private static async Task GetHttpRequestResponseAsync(HttpC } } - using MemoryStream reader = new MemoryStream(File.ReadAllBytes(input.FilePath)); + using MemoryStream reader = new(File.ReadAllBytes(input.FilePath)); using HttpContent content = new StreamContent(reader); var headerDict = input.Headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase); @@ -118,59 +111,4 @@ private static async Task GetHttpRequestResponseAsync(HttpC return response; } -} - -/// -/// Provides extension methods for various types, allowing for additional functionality to be added to existing types. -/// -internal static class HttpClientHandlerExtensions -{ - internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) - { - switch (options.Authentication) - { - case Authentication.WindowsIntegratedSecurity: - handler.UseDefaultCredentials = true; - break; - case Authentication.WindowsAuthentication: - var domainAndUserName = options.Username.Split('\\'); - if (domainAndUserName.Length != 2) - { - throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); - } - handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); - break; - case Authentication.ClientCertificate: - handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); - break; - } - - handler.AllowAutoRedirect = options.FollowRedirects; - - if (options.AllowInvalidCertificate) - { - handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; - } - } - - internal static X509Certificate2 GetCertificate(string thumbprint) - { - thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - try - { - store.Open(OpenFlags.ReadOnly); - var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); - if (signingCert.Count == 0) - { - throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); - } - - return signingCert[0]; - } - finally - { - store.Close(); - } - } } \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs new file mode 100644 index 0000000..c654bbd --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile/HttpClientHandlerExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using Frends.HTTP.UploadFile.Definitions; + +namespace Frends.HTTP.UploadFile; + +/// +/// Provides extension methods for various types, allowing for additional functionality to be added to existing types. +/// +internal static class HttpClientHandlerExtensions +{ + internal static void SetHandleSettingsBasedOnOptions(this HttpClientHandler handler, Options options) + { + switch (options.Authentication) + { + case Authentication.WindowsIntegratedSecurity: + handler.UseDefaultCredentials = true; + break; + case Authentication.WindowsAuthentication: + var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) + { + throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); + } + handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; + case Authentication.ClientCertificate: + handler.ClientCertificates.Add(GetCertificate(options.CertificateThumbprint)); + break; + } + + handler.AllowAutoRedirect = options.FollowRedirects; + + if (options.AllowInvalidCertificate) + { + handler.ServerCertificateCustomValidationCallback = (a, b, c, d) => true; + } + } + + internal static X509Certificate2 GetCertificate(string thumbprint) + { + thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); + var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + store.Open(OpenFlags.ReadOnly); + var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) + { + throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + } + + return signingCert[0]; + } + finally + { + store.Close(); + } + } +} diff --git a/Frends.HTTP.UploadFile/LICENSE b/Frends.HTTP.UploadFile/LICENSE deleted file mode 100644 index fcf70d9..0000000 --- a/Frends.HTTP.UploadFile/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Community HiQ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From 139e48b7dae44088ab02c94995ab72a06a7127be Mon Sep 17 00:00:00 2001 From: "mateusz.wojtania" Date: Fri, 10 Jan 2025 09:40:11 +0100 Subject: [PATCH 7/8] add coverlet --- .../Frends.HTTP.UploadFile.Tests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj index abae24e..65c43ec 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj @@ -17,6 +17,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From dcbb9fe0aaac66216351bb1eeccc5fc907c3c851 Mon Sep 17 00:00:00 2001 From: "mateusz.wojtania" Date: Fri, 10 Jan 2025 14:17:46 +0100 Subject: [PATCH 8/8] improve test coverage --- .../CertificateHandler.cs | 57 ++++++ .../Frends.HTTP.UploadFile.Tests.csproj | 1 + .../IntegrationTest.cs | 166 ++++++++++++++++-- 3 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs new file mode 100644 index 0000000..d68f711 --- /dev/null +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/CertificateHandler.cs @@ -0,0 +1,57 @@ +using Pluralsight.Crypto; +using System; +using System.IO; +using System.Security.Cryptography.X509Certificates; + +namespace Frends.HTTP.UploadFile.Tests; + +public static class CertificateHandler +{ + public static string Handle(string path, string password, bool cleanUp, string thumbPrint) + { + try + { + if (!cleanUp) + { + using CryptContext ctx = new(); + ctx.Open(); + + X509Certificate2 cert = ctx.CreateSelfSignedCertificate( + new SelfSignedCertProperties + { + IsPrivateKeyExportable = true, + KeyBitLength = 4096, + Name = new X500DistinguishedName("cn=localhost"), + ValidFrom = DateTime.Today.AddDays(-1), + ValidTo = DateTime.Today.AddMinutes(1), + }); + + byte[] certData = cert.Export(X509ContentType.Pfx, password); + + using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + store.Add(cert); + } + + File.WriteAllBytes(path, certData); + return cert.Thumbprint; + } + else + { + using X509Store store = new(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived); + X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false); + + foreach (var cert in col) + store.Remove(cert); + + return null; + } + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } + } +} \ No newline at end of file diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj index 65c43ec..b7d0f03 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/Frends.HTTP.UploadFile.Tests.csproj @@ -22,6 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs index 85be473..61bb65f 100644 --- a/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs +++ b/Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using System; using System.IO; +using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,37 +16,173 @@ namespace Frends.HTTP.UploadFile.Tests; public class IntegrationTest { private IHttpServer _stubHttp; + private static readonly string testFilePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); + private static readonly string testCodePageName = "iso-8859-1"; + private static readonly string expectedContentType = $"text/plain; charset={testCodePageName}"; + private static readonly Header contentTypeHeader = new() { Name = "cONtENT-tYpE", Value = expectedContentType }; + + private static readonly Input defaultInput = new() + { + Method = Method.POST, + Url = "http://localhost:9191/endpoint", + Headers = new Header[1] { contentTypeHeader }, + FilePath = testFilePath + }; + [SetUp] public void Setup() { _stubHttp = HttpMockRepository.At("http://localhost:9191"); + _stubHttp.Stub(x => x.Post("/endpoint")) + .AsContentType($"text/plain; charset={testCodePageName}") + .Return("foo ���") + .OK(); + _stubHttp.Stub(x => x.Post("/wrong-endpoint")) + .AsContentType($"text/plain") + .Return("error occured") + .NotFound(); } [Test] public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() { - var filePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt")); - var codePageName = "iso-8859-1"; - var utf8ByteArray = File.ReadAllBytes(filePath); - var expectedContentType = $"text/plain; charset={codePageName}"; + var utf8ByteArray = File.ReadAllBytes(testFilePath); - _stubHttp.Stub(x => x.Post("/endpoint")) - .AsContentType($"text/plain; charset={codePageName}") - .Return("foo ���") - .OK(); + var result = await UploadFileTask.UploadFile(defaultInput, new Options(), CancellationToken.None); - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[1] { contentType }, FilePath = filePath }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; - var result = await UploadFileTask.UploadFile(input, options, CancellationToken.None); var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); var requestHead = request.RequestHead; - var requestBodyByteArray = Encoding.GetEncoding(codePageName).GetBytes(request.Body); + var requestBodyByteArray = Encoding.GetEncoding(testCodePageName).GetBytes(request.Body); var requestContentType = requestHead.Headers["cONTENT-tYpE"]; - //Casing should not affect setting header. Assert.That(requestContentType, Is.EqualTo(expectedContentType)); Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray)); } + + [Test] + public void ThrowOnErrorResponse() + { + var input = defaultInput; + input.Url = "http://localhost:9191/wrong-endpoint"; + var options = new Options + { + ThrowExceptionOnErrorResponse = true + }; + + Assert.ThrowsAsync(() => UploadFileTask.UploadFile(input, options, CancellationToken.None)); + } + + [Test] + public async Task AddBasicAuthHeader() + { + var options = new Options + { + Authentication = Authentication.Basic, + Username = "foo", + Password = "bar", + }; + var encodedValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}")); + + var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Basic {encodedValue}")); + } + + [Test] + public async Task AddOAuthHeader() + { + var options = new Options + { + Authentication = Authentication.OAuth, + Token = "foobar" + }; + + var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None); + + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Bearer foobar")); + } + + [Test] + public async Task AddInvalidHeader() + { + Header invalidHeader = new() { Name = string.Empty, Value = "bar" }; + var input = defaultInput; + input.Headers = new Header[2] { contentTypeHeader, invalidHeader }; + + var result = await UploadFileTask.UploadFile(input, new Options(), CancellationToken.None); + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + var isInvalidAdded = request.RequestHead.Headers.Any(x => x.Key == string.Empty); + Assert.IsFalse(isInvalidAdded); + } + + [Test] + public async Task AddWindowsIntegratedSecurity() + { + var options = new Options + { + Authentication = Authentication.WindowsIntegratedSecurity + }; + + var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None); + + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + } + + [Test] + public async Task AddWindowsAuthentication() + { + var options = new Options + { + Authentication = Authentication.WindowsAuthentication, + Username = "user\\domain" + }; + + var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None); + + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + } + + [Test] + public void ThrowWhenAddWindowsAuthenticationWithInvalidUsername() + { + var options = new Options + { + Authentication = Authentication.WindowsAuthentication, + Username = "userWithoutDomain" + }; + + Assert.ThrowsAsync(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None)); + } + + [Test] + public async Task AddClientCertiface() + { + string certPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "certwithpk.pfx")); + string certPassword = "password"; + var thumprint = CertificateHandler.Handle(certPath, certPassword, false, null); + + var options = new Options + { + Authentication = Authentication.ClientCertificate, + CertificateThumbprint = thumprint + }; + + var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None); + + var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest(); + } + + [Test] + public void ThrowWhenInvalidClientCertificate() + { + var options = new Options + { + Authentication = Authentication.ClientCertificate, + CertificateThumbprint = "thumbprint" + }; + + Assert.ThrowsAsync(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None)); + } } \ No newline at end of file