using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Http.Models; using Elsa.Activities.Http.Services; using Elsa.Attributes; using Elsa.Design; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; using Microsoft.AspNetCore.Http; namespace Elsa.Activities.Http.Activities { [ActivityDefinition( Category = "HTTP", DisplayName = "Send HTTP Request", Description = "Send an HTTP request.", RuntimeDescription = "x => !!x.state.url ? `Send HTTP ${ x.state.method } ${ x.state.url.expression }.` : x.definition.description", Outcomes = "x => !!x.state.supportedStatusCodes ? x.state.supportedStatusCodes : []" )] public class SendHttpRequest : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; private readonly HttpClient httpClient; private readonly IEnumerable parsers; public SendHttpRequest( IWorkflowExpressionEvaluator expressionEvaluator, IHttpClientFactory httpClientFactory, IEnumerable parsers) { this.expressionEvaluator = expressionEvaluator; httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest)); this.parsers = parsers; } /// /// The URL to invoke. /// [ActivityProperty(Hint = "The URL to send the HTTP request to.")] public WorkflowExpression Url { get { var url = GetState>(); if (url != null && !string.IsNullOrWhiteSpace(url.Expression) && Uri.TryCreate( url.Expression, UriKind.RelativeOrAbsolute, out var uri )) return new WorkflowExpression(url.Syntax, uri.ToString()); return new WorkflowExpression(LiteralEvaluator.SyntaxName, ""); } set => SetState(new WorkflowExpression(value.Syntax, value.Expression)); } /// /// The HTTP method to use. /// [ActivityProperty( Type = ActivityPropertyTypes.Select, Hint = "The HTTP method to use when making the request." )] [SelectOptions("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD")] public string Method { get => GetState(() => "GET"); set => SetState(value); } /// /// The body to send along with the request. /// [ActivityProperty(Hint = "The HTTP content to send along with the request.")] [ExpressionOptions(Multiline = true)] public WorkflowExpression Content { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } /// /// The Content Type header to send along with the request body. /// [ActivityProperty( Type = ActivityPropertyTypes.Select, Hint = "The content type to send with the request (if applicable)." )] [SelectOptions("text/plain", "text/html", "application/json", "application/xml")] public string ContentType { get => GetState(() => "text/plain"); set => SetState(value); } [ActivityProperty( Hint = "The Authorization header value to send." )] public WorkflowExpression Authorization { get => GetState>(); set => SetState(value); } /// /// The headers to send along with the request. /// [ActivityProperty(Hint = "The headers to send along with the request. One 'header: value' pair per line.")] public WorkflowExpression RequestHeaders { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } [ActivityProperty(Hint = "Check to read the content of the response.")] public bool ReadContent { get => GetState(() => true); set => SetState(value); } /// /// A list of HTTP status codes this activity can handle. /// [ActivityProperty( Type = ActivityPropertyTypes.List, Hint = "A list of possible HTTP status codes to handle, comma-separated. Example: 200, 400, 404" )] public HashSet SupportedStatusCodes { get => GetState(() => new HashSet { 200 }); set => SetState(value); } protected override async Task OnExecuteAsync( WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) { var request = await CreateRequestAsync(workflowContext, cancellationToken); var response = await httpClient.SendAsync(request, cancellationToken); var hasContent = response.Content != null; var contentType = response.Content?.Headers.ContentType.MediaType; var responseModel = new HttpResponseModel { StatusCode = response.StatusCode, Headers = new Dictionary( response.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray()) ) }; if (hasContent && ReadContent) { var formatter = SelectContentParser(contentType); responseModel.Content = await formatter.ParseAsync(response, cancellationToken); } workflowContext.SetLastResult(Output.SetVariable("Response", responseModel)); var statusEndpoint = ((int)response.StatusCode).ToString(); return Outcomes(new[] { OutcomeNames.Done, statusEndpoint }); } private IHttpResponseBodyParser SelectContentParser(string contentType) { var formatters = parsers.OrderByDescending(x => x.Priority).ToList(); return formatters.FirstOrDefault( x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase) ) ?? formatters.Last(); } private async Task CreateRequestAsync( WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) { var methodSupportsBody = GetMethodSupportsBody(Method); var uri = await expressionEvaluator.EvaluateAsync(Url, workflowContext, cancellationToken); var request = new HttpRequestMessage(new HttpMethod(Method), uri); var authorizationHeaderValue = await expressionEvaluator.EvaluateAsync( Authorization, workflowContext, cancellationToken ); var requestHeaders = await ParseRequestHeadersAsync(workflowContext, cancellationToken); if (methodSupportsBody) { var body = await expressionEvaluator.EvaluateAsync(Content, workflowContext, cancellationToken); var contentType = ContentType; if (!string.IsNullOrWhiteSpace(body)) { request.Content = new StringContent(body, Encoding.UTF8, contentType); } } if (!string.IsNullOrWhiteSpace(authorizationHeaderValue)) { request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorizationHeaderValue); } foreach (var header in requestHeaders) { request.Headers.Add(header.Key, header.Value.AsEnumerable()); } return request; } private async Task ParseRequestHeadersAsync( WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) { var headersText = await expressionEvaluator.EvaluateAsync( RequestHeaders, workflowContext, cancellationToken ); var headers = new HeaderDictionary(); if (headersText != null) { var headersQuery = from line in Regex.Split(headersText, "\\n", RegexOptions.Multiline) let pair = line.Split(':', '=') select new KeyValuePair(pair[0], pair[1]); foreach (var header in headersQuery) { var headerValueExpression = new WorkflowExpression(RequestHeaders.Syntax, header.Value); var headerValue = await expressionEvaluator.EvaluateAsync( headerValueExpression, workflowContext, cancellationToken ); headers.Add(header.Key, headerValue); } } return headers; } private bool GetMethodSupportsBody(string method) { var methods = new[] { "POST", "PUT", "PATCH" }; return methods.Contains(method, StringComparer.InvariantCultureIgnoreCase); } } }