Table of Contents

Custom filters and actions

fluxzy provides a set of built-in actions and filters that can be used in a straightforward manner.

However, there may be cases where you need to perform more complex operations on an exchange.

This page will try to guide you on how to setup low level directives by implementing custom action and filter.

Creating a custom action or filter

To create a custom action or filter, you need to inherit respectively Fluxzy.Rules.Action or Fluxzy.Rules.Filter and override the required methods.

Fluxzy .NET libraries are nullable-friendly, so you can use nullable reference types in your custom directive.

A new action implementation must define at least the following methods and properties:

  • Description property: which will be used used for identification.
  • ActionScope property: which must return a FilterScope defining when, during an execution, the action shall be executed. In the code snippet above, FilterScope.OnAuthorityReceived will make the action to be executed when fluxzy knows the destination authority.
  • InternalAlter() method: which contains the logic of how the exchange should be altered.

Here as example that showcases various alteration that you can apply to an exchange:

using Fluxzy.Core;
using Fluxzy.Core.Breakpoints;
using Fluxzy.Rules;
using Action = Fluxzy.Rules.Action;

namespace low_level_filters;

public class ShowCaseAction : Action
{
    public override FilterScope ActionScope => FilterScope.OnAuthorityReceived;

    public override string DefaultDescription => nameof(ShowCaseAction);

    public override ValueTask InternalAlter(ExchangeContext context, Exchange? exchange, Connection? connection,
        FilterScope scope,
        BreakPointManager breakPointManager)
    {
        // Uncomment this line to abort the connection
        // context.Abort = true;

        // Uncomment this line to skip SSL decryption
        // context.BlindMode = true; 

        // Uncomment this line to add a client certificate to the request
        // context.ClientCertificates ??= new();
        // context.ClientCertificates.Add(Certificate.LoadFromPkcs12("my-pkcs-12.p12", "password"));

        // Uncomment this line to add a server certificate to the action
        // context.ServerCertificate = new X509Certificate2("domain-cert.pfx", "password")

        // Add a new request header 
        // context.RequestHeaderAlterations.Add(new HeaderAlterationReplace("User-Agent", "My Custom Hook", true));

        // Explore provided argument to see all possible actions

        return default;
    }
}

ExchangeContext is a mutable object that is passed to the InternalAlter method. It contains the current state of the exchange and is used to alter the exchange. You can freely browse its properties to know what could be altered or not.

Alternatively for a new filter, we need to inherit Fluxzy.Rules.Filter class and override the required methods and properties. The main difference is that InternalApply must return a bool value indicating if the filter is matched or not.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Fluxzy;
using Fluxzy.Core;
using Fluxzy.Rules;
using Fluxzy.Rules.Filters;

namespace low_level_filters
{
    public class ShowCaseFilter : Filter
    {
        protected override bool InternalApply(ExchangeContext? exchangeContext, IAuthority authority, IExchange? exchange,
            IFilteringContext? filteringContext)
        {
            // Should returns true 

            return true;
        }

        public override IEnumerable<FilterExample> GetExamples()
        {
            yield break;
        }

        /// <summary>
        ///  Indicated the timing where this filter is evaluated
        /// </summary>
        public override FilterScope FilterScope => FilterScope.RequestHeaderReceivedFromClient;
    }
}

Now in order to use the new action or filter, you need to register it to the proxy.

var fluxzySetting = FluxzySetting.CreateDefault();

fluxzySetting.ConfigureRule()
    .When(new ShowCaseFilter())
    .Do(new ShowCaseAction());

You can see here that the lifecycle of a custom directive is bound to the lifecycle of a configuration so you must consider concurrency issue if your directives relies on a global state.

IStreamSubstitution

Note

fluxzy is, per default, always in full streaming mode for request and body response. That means, that at no time during the processing of any exchange, a full snapshot of a body larger than the default buffer size is available. If you need filters and actions that needs the full content request/response body you must implement the buffering yourself.

You can set up an IStreamSubstitution If you need to access the request or the response body during the processing of an exchange.

A stream substitution is a straightforward way to replace the original stream with a new one. The IStreamSubstitution is a simple interface with an unique required method that you can used withing the ExchangeContext methods ExchangeContext.RegisterRequestBodySubstitution() and ExchangeContext.RegisterResponseBodySubstitution. However, in order to no make the original connection linger, the original stream must be drained before the result stream goes EOF.

You can register as much substitution as you want. Each substitution will be executed in the order they were registered.

  • IStreamSubstitution or mocked body ? The main difference between IStreamSubstitution and mocked body is that the later is completely independent from the original stream. That means also that no network interaction will be made to the original destination.
  • Body is already decoded the provided stream is already decoded if the original encoding is in the following list: gzip, brotli, deflate. The same applies also for chunked encoding which is already removed.
  • You can check InjectHtmlTagAction as an example of using a stream substitution to inject a code snippet an html page.

Here is how we set a simple response mock with a custom filters:

Create an IStreamSubstitution implementation:

using System.Text;
using Fluxzy.Core;
using Fluxzy.Misc.Streams;

namespace low_level_filters;

internal class GreedyMockSubstitution : IStreamSubstitution
{
    public async ValueTask<Stream> Substitute(Stream originalStream)
    {
        // We must drain the original stream to prevent the remote connection from hanging
        // DrainAsync is an extension method from Fluxzy.Misc.Streams.StreamExtensions that will read
        // the stream until EOF and return the content length

        await originalStream.DrainAsync();

        // Return a new stream with a mocked response for the sake of this example
        return new MemoryStream(Encoding.UTF8.GetBytes("This response wash mocked"));
    }
}

When the action triggers, set it as the response body substitution:

using Fluxzy.Core;
using Fluxzy.Core.Breakpoints;
using Fluxzy.Rules;
using Action = Fluxzy.Rules.Action;

namespace low_level_filters;

internal class GreedyMockAction : Action
{
    public override ValueTask InternalAlter(ExchangeContext context, Exchange? exchange, Connection? connection, FilterScope scope,
        BreakPointManager breakPointManager)
    {
        context.RegisterResponseBodySubstitution(new GreedyMockSubstitution());
        return default;
    }

    public override FilterScope ActionScope => FilterScope.ResponseHeaderReceivedFromRemote;

    public override string DefaultDescription =>  nameof(GreedyMockAction);
}

Now, you can use the regular method FluxzySetting.AddAlterationRules to add the action to the proxy.

using Fluxzy;
using low_level_filters;

var fluxzySetting = FluxzySetting.CreateDefault();

fluxzySetting.ConfigureRule()
    .When(new ShowCaseFilter())
    .Do(new ShowCaseAction());

// We add our custom action to the default setting
fluxzySetting.AddAlterationRulesForAny(new GreedyMockAction());

// Create a new proxy instance
await using (var _ = new Proxy(fluxzySetting))
{
    Console.WriteLine("Press any key to exit");
}