// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.ComponentModel.DataAnnotations; using System.Text; using Squidex.Domain.Apps.Core.Rules.Deprecated; using Squidex.Flows; using Squidex.Flows.Steps.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; #pragma warning disable CS0618 // Type or member is obsolete namespace Squidex.Extensions.Actions.Webhook; [FlowStep( Title = "Webhook", IconImage = "", IconColor = "#4bb958", Display = "Send webhook", Description = "Invoke HTTP endpoints on a target system.", ReadMore = "https://en.wikipedia.org/wiki/Webhook")] public sealed record WebhookFlowStep : FlowStep, IConvertibleToAction { [LocalizedRequired] [Display(Name = "Method", Description = "The type of the request.")] public WebhookMethod Method { get; set; } [LocalizedRequired] [Display(Name = "Url", Description = "The URL to the webhook.")] [Expression] public Uri Url { get; set; } [Expression(ExpressionFallback.Envelope)] [Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] [Editor(FlowStepEditor.TextArea)] public string? Payload { get; set; } [Expression] [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] [Editor(FlowStepEditor.TextArea)] public string? Headers { get; set; } [Display(Name = "Payload Type", Description = "The mime type of the payload.")] [Editor(FlowStepEditor.Text)] public string? PayloadType { get; set; } [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")] [Editor(FlowStepEditor.Text)] public string? SharedSecret { get; set; } public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, CancellationToken ct) { var method = HttpMethod.Post; switch (Method) { case WebhookMethod.PUT: method = HttpMethod.Put; break; case WebhookMethod.GET: method = HttpMethod.Get; break; case WebhookMethod.DELETE: method = HttpMethod.Delete; break; case WebhookMethod.PATCH: method = HttpMethod.Patch; break; } string? requestBody = null; var request = new HttpRequestMessage(method, Url); if (!string.IsNullOrEmpty(Payload) && Method != WebhookMethod.GET) { var mediaType = PayloadType; if (string.IsNullOrEmpty(mediaType)) { mediaType = "application/json"; } requestBody = Payload; request.Content = new StringContent(Payload, Encoding.UTF8, mediaType); } var headers = ParseHeaders(); if (headers != null) { foreach (var (key, value) in headers) { request.Headers.TryAddWithoutValidation(key, value); } } if (!string.IsNullOrWhiteSpace(SharedSecret)) { var signature = $"{Payload}{SharedSecret}".ToSha256Base64(); request.Headers.Add("X-Signature", signature); } if (executionContext.IsSimulation) { executionContext.LogSkipSimulation( HttpDumpFormatter.BuildDump(request, null, requestBody, null)); return Next(); } var httpClient = executionContext.Resolve().CreateClient("FlowClient"); var (_, dump) = await httpClient.SendAsync(executionContext, request, requestBody, ct); executionContext.Log("HTTP request sent", dump); return Next(); } private Dictionary? ParseHeaders() { if (string.IsNullOrWhiteSpace(Headers)) { return null; } var headersDictionary = new Dictionary(); var lines = Headers.Split('\n'); foreach (var line in lines) { var indexEqual = line.IndexOf('=', StringComparison.Ordinal); if (indexEqual > 0 && indexEqual < line.Length - 1) { var headerKey = line[..indexEqual]; var headerValue = line[(indexEqual + 1)..]; headersDictionary[headerKey] = headerValue!; } } return headersDictionary; } public RuleAction ToAction() { return SimpleMapper.Map(this, new WebhookAction()); } }