Cloudflare API - Purge Files By URL In C#

Earlier this week I wrote about the reasons to why I decided to use Cloudflare for my website. I've been working on utilising Cloudflare's API to purge the cache on demand for when files need to be updated within the CDN. To do this, I decided to write a method that will primarily use one API endpoint - /purge_cache. This endpoint allows a maximum of 30 URL's at one time to be purged, which is flexible enough to fit the majority of day-to-day use cases.

To communicate with the API, we need to provide three pieces of information:

  1. Account Email Address
  2. Zone ID
  3. API Key

The last two pieces of information can be found within the dashboard of your Cloudflare account.

Code - CloudflareCacheHelper Class

The CloudflareCacheHelper class consists of a single method PurgeSelectedFiles() and the following class objects used for serializing and deserializing our responses from API requests:

  • CloudflareFileInfo
  • CloudflareZone
  • CloudflareResultInfo
  • CloudflareResponse

Not all the properties within each of the class objects are being used at the moment based on the requests I am making. But the CloudflareCacheHelper class will be updated with more methods as I delve further into Cloudflare's functionality.

public class CloudflareCacheHelper
{
    public string _userEmail;
    public string _apiKey;
    public string _zoneId;

    private readonly string ApiEndpoint = "https://api.cloudflare.com/client/v4";

    /// <summary>
    /// By default the Cloudflare API values will be taken from the Web.Config.
    /// </summary>
    public CloudflareCacheHelper()
    {
        _apiKey = ConfigurationManager.AppSettings["Cloudflare.ApiKey"];
        _userEmail = ConfigurationManager.AppSettings["Cloudflare.UserEmail"];
        _zoneId = ConfigurationManager.AppSettings["Cloudflare.ZoneId"];
    }

    /// <summary>
    /// Set the Cloudflare API values explicitly.
    /// </summary>
    /// <param name="userEmail"></param>
    /// <param name="apiKey"></param>
    /// <param name="zoneId"></param>
    public CloudflareCacheHelper(string userEmail, string apiKey, string zoneId)
    {
        _userEmail = userEmail;
        _apiKey = apiKey;
        _zoneId = zoneId;
    }
        
    /// <summary>
    /// A collection of file paths (max of 30) will be accepted for purging cache.
    /// </summary>
    /// <param name="filePaths"></param>
    /// <returns>Boolean value on success or failure.</returns>
    public bool PurgeSelectedFiles(List<string> filePaths)
    {
        CloudflareResponse purgeResponse = null;

        if (filePaths?.Count > 0)
        {
            try
            {
                HttpWebRequest purgeRequest = WebRequest.CreateHttp($"{ApiEndpoint}/zones/{_zoneId}/purge_cache");
                purgeRequest.Method = "POST";
                purgeRequest.ContentType = "application/json";
                purgeRequest.Headers.Add("X-Auth-Email", _userEmail);
                purgeRequest.Headers.Add("X-Auth-Key", _apiKey);

                #region Create list of Files for Submission In The Structure The Response Requires

                CloudflareFileInfo fileInfo = new CloudflareFileInfo
                {
                    Files = filePaths
                };

                byte[] data = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(fileInfo));

                purgeRequest.ContentLength = data.Length;

                using (Stream fileStream = purgeRequest.GetRequestStream())
                {
                    fileStream.Write(data, 0, data.Length);
                    fileStream.Flush();
                }

                #endregion

                using (WebResponse response = purgeRequest.GetResponse())
                {
                    using (StreamReader purgeStream = new StreamReader(response.GetResponseStream()))
                    {
                        string responseJson = purgeStream.ReadToEnd();

                        if (!string.IsNullOrEmpty(responseJson))
                            purgeResponse = JsonConvert.DeserializeObject<CloudflareResponse>(responseJson);
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return purgeResponse.Success;
        }

        return false;
    }

    #region Cloudflare Class Objects

    public class CloudflareFileInfo
    {
        [JsonProperty("files")]
        public List<string> Files { get; set; }
    }

    public class CloudflareZone
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("content")]
        public string Content { get; set; }

        [JsonProperty("proxiable")]
        public bool Proxiable { get; set; }

        [JsonProperty("proxied")]
        public bool Proxied { get; set; }

        [JsonProperty("ttl")]
        public int Ttl { get; set; }

        [JsonProperty("priority")]
        public int Priority { get; set; }

        [JsonProperty("locked")]
        public bool Locked { get; set; }

        [JsonProperty("zone_id")]
        public string ZoneId { get; set; }

        [JsonProperty("zone_name")]
        public string ZoneName { get; set; }

        [JsonProperty("modified_on")]
        public DateTime ModifiedOn { get; set; }

        [JsonProperty("created_on")]
        public DateTime CreatedOn { get; set; }
    }

    public class CloudflareResultInfo
    {
        [JsonProperty("page")]
        public int Page { get; set; }

        [JsonProperty("per_page")]
        public int PerPage { get; set; }

        [JsonProperty("count")]
        public int Count { get; set; }

        [JsonProperty("total_count")]
        public int TotalCount { get; set; }
    }

    public class CloudflareResponse
    {
        [JsonProperty("result")]
        public CloudflareZone Result { get; set; }

        [JsonProperty("success")]
        public bool Success { get; set; }

        [JsonProperty("errors")]
        public IList<object> Errors { get; set; }

        [JsonProperty("messages")]
        public IList<object> Messages { get; set; }

        [JsonProperty("result_info")]
        public CloudflareResultInfo ResultInfo { get; set; }
    }

    #endregion
}

Example - Purging Cache of Two Files

A string collection of URL's can be passed into the method to allow for the cache of a batch of files to be purged in a single request. If all goes well, the success response should be true.

CloudflareCacheHelper cloudflareCache = new CloudflareCacheHelper();

bool isSuccess = cloudflareCache.PurgeSelectedFiles(new List<string> {
                                    "https://www.surinderbhomra.com/getmedia/7907d934-805f-4bd3-86e7-a6b2027b4ba6/CloudflareResponseMISS.png",
                                    "https://www.surinderbhomra.com/getmedia/89679ffc-ca2f-4c47-8d41-34a6efdf7bb8/CloudflareResponseHIT.png"
                                });

Rate Limits

The Cloudflare API sets a maximum of 1,200 requests in a five minute period. Cache-Tag purging has a lower rate limit of up to 2,000 purge API calls in every 24 hour period. You may purge up to 30 tags in one API call.

My Reasons for Using Cloudflare With Kentico

A couple day ago my website got absolutely hammered by a wave of constant SQL injection attacks by the same IP over a time period of a couple hours.

I only managed to notice this whilst perusing the Event Log within the Kentico Administration interface. I don't normally check my own error logs as regularly as I should do, but since my site has recently gone through a bit of a revamp (which I'm still yet to post about), I wanted to ensure I haven't broken anything.

To be honest, I am flattered that someone would think this site is worth the time and energy in trying to hack my site. Trust me, it ain't worth it.

Even though Kentico has handled these attacks well, as a precaution I wanted to implement an additional layer of security before any further untoward activity reaches to my site. Being on a shared hosting platform my options are limited and my hands are tied to put in an infrastructure that doesn't cost the world.

Enter Cloudflare

I have always had some form of awareness of the Cloudflare content delivery network, just never put the service into practice. At one point I was looking into utilising Cloudflare to manage all my site media files over their CDN. But Cloudflare isn't just a CDN, it's able to offer much more:

  • Analytics - monitor traffic as well as caching ratio and more!
  • Firewall - manage access by IP, country, or query rules.
  • Rate limiting - protect your site or API from malicious traffic by blocking client IP addresses that hit a URL pattern and exceed a threshold you define.
  • Page rules - to allow caching to be triggered by a number of rules targeting specific areas of a site.

The great thing is that these options are part of the free plan... even though there are restrictions to the number of settings you are able to put in place. The security options alone was enough of a reason to try out Cloudflare. But for my needs, the free plan seemed to tick all the boxes. I am just scratching the surface to what Cloudflare has to offer and have already got a few of the features working alongside Kentico.

How I'm Using Cloudflare With Kentico

As security was a main concern for me, one of the first things I did was to add in some rules through the firewall and block suspicious traffic. Naturally the next was to take advantage of the CDN capabilities. I wouldn't recommend full site caching unless you have some pretty strict page rules in place as this has a chance to cause issues with the Kentico Admin interface. By default, Cloudflare doesn't cache HTML pages, which is a good thing as it gives us a plain canvas to target the areas we want to cache.

Being on the free plan, I only had three page rules at my disposal and made the decision to cache the following parts of my site.

Area Regex Rule Settings
Media Library Files (using Permanent URL's) *surinderbhomra.com/
getmedia/*

Cache Level: Caches Everything
Edge Cache TTL: A month
Browser Cache TTL: 16 days

Site CSS, JavaScript and Images *surinderbhomra.com/
resources/*
Cache Level: Caches Everything
Edge Cache TTL: A month
Browser Cache TTL: 16 days
ScriptResource.axd/WebResource.axd *surinderbhomra.com/*.axd Cache Level: Caches Everything
Edge Cache TTL: 7 days
Browser Cache TTL: 7 days

Two cache types are used:

  1. Edge Cache TTL - is the setting that controls how long CloudFlare's edge servers will cache a resource before requesting a fresh copy from your server. 
  2. Browser Cache TTL - is the time that CloudFlare instructs a visitor's browser to cache a resource. Until this time expires, the browser will load the resource from its local cache and speeding up the request.

I am caching my assets for a considerable amount of time, which begs the question how will changes to files purge the cache in Cloudflare? Luckily, Cloudflare has quite a nice API, where I have the ability to purge everything or individual files (maximum of 30 in one request).

Purge Media Library File Cache

I am in the middle of testing a GlobalEvent handler that will carry out the following steps when files are inserted or updated:

  • Get path of the file.
  • Check if site is using Permanent URL's. As the URL to the file will be constructured differently if enabled.
  • Convert the relative path to an absolute path.
  • Pass the absolute file path to Cloudflare using the following Purge Files by URL API endpoint.

Once I have carried out some further tests, I will be posting this code in a follow-up post.

Purge Site File Cache

Now attempting to purge the cache for site files such as JavaScript, CSS and images are a little more tricky as I need to keep an eye on the files changed. The easiest thing to do is I could write some code that will iterate through all the files in the /resources folder and purge everything from the CDN. Not the most elegant solution. Still, need to ponder on the correct implementation. If anyone has got any better suggestions, let me know.

How Do I Know Cloudflare Is Caching My Site Files?

It does take a little time in Cloudflare to cache all files on a site. It all depends on pages being viewed. A page needs to be loaded in order to submit all its contents to cache. On first load, the response header CF-Cache-Status will return a "MISS", which means the content has not been served from Cloudflare.

Cloudflare Response - MISS

However, when you go back to the page and re-check the page headers, the CF-Cache-Status should return "HIT". If this is not the case, check your Page Rules within the Cloudflare dashboard.

Cloudflare Response - HIT

Is Cloudflare Worth It?

Quick answer - Yes!

Setup is very straight-forward. All that is required is to carry out a change to your domain and point your DNS to the DNS Cloudflare assigns to you. There is no downtime in doing this. As a result, overall site performance has improved and page speed test faired much better.

To give you a better insight for transparency, here are some statistics straight from my Cloudflare portal over a 24 hour period:

Requests Through Cloudflare
(Understandably, the number of cached items served through Cloudflare is low due to only caching specific areas)

Cloudflare Performance
(My basic shared hosting should now be performing better as less requests are being served via the origin server)

Cloudflare Detected Threats
(Blocked threats - the reason why I decided to give Cloudflare a try in the first place)

I still have to carry out a lot more research in using Cloudflare to its full potential and will use my website as a test bed to see what I can achieve. My end goal is to make my website quicker and more secure!

Autoplaying HTML5 Video In Chrome

Whilst working on the new look for my website, I wanted to replace areas where I previously used low-grade animated GIF's for the more modern HTML5 video. Currently, the only place I use HTML5 video is on my 404 page as a light-hearted reference to one of the many memorable quotes that only fans of the early Star Trek films will understand. These are the films I still hold in very high regard, something the recent "kelvin timeline" films are missing. Anyway, back to the post in hand...

Based on Chrome's new policies introduced in April 2018 I was always under the impression that as long as the video is muted, this won't hinder in any way the autoplay functionality. But for the life of me, mt HTML5 video did not autoplay, even though all worked as intended in other browsers such as Firefox.

You can work around Chrome's restrictions through JavaScript.

Code

The HTML is as simple as adding your HTML5 video.

<video id="my-video" autoplay muted loop playsinline>
     <source src="/enterprise-destruction.mp4" type="video/mp4" />
</video>

All we need to do is target our video and tell it to play automatically. I have added a timeout to the script just to ensure the video has enough time to render on the page before our script can do its thing.

var myVideo = $("#my-video");

setTimeout(function () {
    myVideo.muted = true;
    myVideo.play();
}, 100);

It's worth noting that I don't generally write much about front-end approaches (excluding JavaScript) as I am first and foremost a backend developer. So this might not be the most ideal solution and appreciate any feedback.