using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Http.UIHints; using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.UIHints; using Elsa.Workflows.Models; using Microsoft.Extensions.Logging; namespace Elsa.Http; /// /// An activity that downloads a file from a given URL. /// [Activity("Elsa", "HTTP", "Downloads a file from a given URL.", DisplayName = "Download File", Kind = ActivityKind.Task)] [Output(IsSerializable = false)] public class DownloadHttpFile : Activity, IActivityPropertyDefaultValueProvider { /// public DownloadHttpFile([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } /// /// The URL to download the file from. /// [Input(DisplayName = "URL", Description = "The URL to download the file from.")] public Input Url { get; set; } = null!; /// /// The HTTP method to use when sending the request. /// [Input( Description = "The HTTP method to use when sending the request.", Options = new[] { "GET", "POST", "PUT" }, DefaultValue = "GET", UIHint = InputUIHints.DropDown )] public Input Method { get; set; } = new("GET"); /// /// A list of expected status codes to handle. /// [Input( Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText, DefaultValueProvider = typeof(FlowSendHttpRequest) )] public Input> ExpectedStatusCodes { get; set; } = null!; /// /// The content to send with the request. Can be a string, an object, a byte array or a stream. /// [Input(Name = "Content", Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")] public Input RequestContent { get; set; } = null!; /// /// The content type to use when sending the request. /// [Input( DisplayName = "Content Type", Description = "The content type to use when sending the request.", UIHandler = typeof(HttpContentTypeOptionsProvider), UIHint = InputUIHints.DropDown )] public Input RequestContentType { get; set; } = null!; /// /// The Authorization header value to send with the request. /// /// Bearer {some-access-token} [Input(Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}", Category = "Security")] public Input Authorization { get; set; } = null!; /// /// A value that allows to add the Authorization header without validation. /// [Input(Description = "A value that allows to add the Authorization header without validation.", Category = "Security")] public Input DisableAuthorizationHeaderValidation { get; set; } = null!; /// /// The headers to send along with the request. /// [Input( Description = "The headers to send along with the request.", UIHint = InputUIHints.JsonEditor, Category = "Advanced" )] public Input RequestHeaders { get; set; } = new(new HttpHeaders()); /// /// The HTTP response. /// [Output(IsSerializable = false)] public Output Response { get; set; } = null!; /// /// The HTTP response status code /// [Output(Description = "The HTTP response status code")] public Output StatusCode { get; set; } = null!; /// /// The downloaded content stream, if any. /// [Output(Description = "The downloaded content stream, if any.", IsSerializable = false)] public Output ResponseContentStream { get; set; } = null!; /// /// The downloaded content bytes, if any. /// [Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)] public Output ResponseContentBytes { get; set; } = null!; /// /// The response headers that were received. /// [Output(Description = "The response headers that were received.")] public Output ResponseHeaders { get; set; } = null!; /// /// The response content headers that were received. /// [Output(DisplayName = "Content Headers", Description = "The response content headers that were received.")] public Output ResponseContentHeaders { get; set; } = null!; /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { await TrySendAsync(context); } private async Task TrySendAsync(ActivityExecutionContext context) { var request = PrepareRequest(context); var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType())); var httpClientFactory = context.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); var cancellationToken = context.CancellationToken; try { var response = await httpClient.SendAsync(request, cancellationToken); var file = await GetFileFromResponse(context, response, request); var statusCode = (int)response.StatusCode; var responseHeaders = new HttpHeaders(response.Headers); var responseContentHeaders = new HttpHeaders(response.Content.Headers); context.Set(Response, response); context.Set(ResponseContentStream, file?.Stream); context.Set(Result, file); context.Set(StatusCode, statusCode); context.Set(ResponseHeaders, responseHeaders); context.Set(ResponseContentHeaders, responseContentHeaders); if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes()); await HandleResponseAsync(context, response); } catch (HttpRequestException e) { logger.LogWarning(e, "An error occurred while sending an HTTP request"); context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Error", e.Message); await HandleRequestExceptionAsync(context, e); } catch (TaskCanceledException e) { logger.LogWarning(e, "An error occurred while sending an HTTP request"); context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Cancelled", true); await HandleTaskCanceledExceptionAsync(context, e); } } /// /// Handles the response. /// private async Task HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) { var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List(0); var statusCode = (int)response.StatusCode; var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode); var outcome = expectedStatusCodes.Any() ? hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code" : null; var outcomes = new List(); if (outcome != null) outcomes.Add(outcome); outcomes.Add("Done"); await context.CompleteActivityWithOutcomesAsync(outcomes.ToArray()); } /// /// Handles an exception that occurred while sending the request. /// private async Task HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception) { await context.CompleteActivityWithOutcomesAsync("Failed to connect"); } /// /// Handles that occurred while sending the request. /// private async Task HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception) { await context.CompleteActivityWithOutcomesAsync("Timeout"); } private async Task GetFileFromResponse(ActivityExecutionContext context, HttpResponseMessage httpResponse, HttpRequestMessage httpRequestMessage) { var httpContent = httpResponse.Content; if (!HasContent(httpContent)) return null; var cancellationToken = context.CancellationToken; var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken); var responseHeaders = httpResponse.Headers; var contentHeaders = httpContent.Headers; var contentType = contentHeaders.ContentType?.MediaType!; var filename = contentHeaders.ContentDisposition?.FileName ?? httpRequestMessage.RequestUri!.Segments.LastOrDefault() ?? "file.dat"; var eTag = responseHeaders.ETag?.Tag; return new HttpFile(contentStream, filename, contentType, eTag); } private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0; private HttpRequestMessage PrepareRequest(ActivityExecutionContext context) { var method = Method.GetOrDefault(context) ?? "GET"; var url = Url.Get(context); var request = new HttpRequestMessage(new HttpMethod(method), url); var headers = context.GetHeaders(RequestHeaders); var authorization = Authorization.GetOrDefault(context); var addAuthorizationWithoutValidation = DisableAuthorizationHeaderValidation.GetOrDefault(context); if (!string.IsNullOrWhiteSpace(authorization)) if (addAuthorizationWithoutValidation) request.Headers.TryAddWithoutValidation("Authorization", authorization); else request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); foreach (var header in headers) request.Headers.Add(header.Key, header.Value.AsEnumerable()); var contentType = RequestContentType.GetOrDefault(context); var content = RequestContent.GetOrDefault(context); if (contentType != null && content != null) { var factories = context.GetServices(); var factory = SelectContentWriter(contentType, factories); request.Content = factory.CreateHttpContent(content, contentType); } return request; } private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable factories) { if (string.IsNullOrWhiteSpace(contentType)) return new JsonContentFactory(); var parsedContentType = new System.Net.Mime.ContentType(contentType); return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory(); } object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property) { if (property.Name == nameof(ExpectedStatusCodes)) return new List { 200 }; return null!; } }