Advertisement
Guest User

CF Worker Template - Device Based Caching

a guest
May 28th, 2021
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // Worker version: 2.6.1
  2. // Default cookie prefixes for cache bypassing
  3. const DEFAULT_BYPASS_COOKIES = [
  4.   'wp-',
  5.   'wordpress_logged_in_',
  6.   'comment_',
  7.   'woocommerce_',
  8.   'wordpressuser_',
  9.   'wordpresspass_',
  10.   'wordpress_sec_',
  11.   'yith_wcwl_products',
  12.   'edd_items_in_cart',
  13.   'it_exchange_session_',
  14.   'comment_author',
  15.   'dshack_level',
  16.   'auth',
  17.   'noaffiliate_',
  18.   'mp_session',
  19.   'mp_globalcart_'
  20. ]
  21.  
  22. // Third party query parameter that we need to ignore in a URL
  23. const THIRD_PARTY_QUERY_PARAMETERS = [
  24.   'fbclid',
  25.   'fb_action_ids',
  26.   'fb_action_types',
  27.   'fb_source',
  28.   '_ga',
  29.   'age-verified',
  30.   'ao_noptimize',
  31.   'usqp',
  32.   'cn-reloaded',
  33.   'klaviyo',
  34.   'gclid',
  35.   'utm_source',
  36.   'utm_medium',
  37.   'utm_campaign',
  38.   'utm_content',
  39.   'utm_term'
  40. ]
  41.  
  42.  
  43.  
  44. function getDeviceType (ua) {
  45.   if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
  46.     return "tablet";
  47.   }
  48.   if (
  49.       /Mobi|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)
  50.   ) {
  51.     return "mobile";
  52.   }
  53.   return "desktop";
  54. }
  55.  
  56. /**
  57.  * Function to normalize the URL by removing promotional query parameters from the URL and cache the original URL
  58.  * @param {Object} event
  59.  */
  60. function url_normalize(event) {
  61.   try {
  62.     // Fetch the Request URL from the event
  63.     // Parse the URL for better handling
  64.     const reqURL = new URL(event.request.url)
  65.     const ua = event.request.headers.get('user-agent');
  66.     const uaValue = getDeviceType(ua);
  67.     reqURL.searchParams.set('device', uaValue);
  68.  
  69.     // Loop through the promo queries (THIRD_PARTY_QUERY_PARAMETERS) and see if we have any of these queries present in the URL, if so remove them
  70.     THIRD_PARTY_QUERY_PARAMETERS.forEach( (queryParam) => {
  71.  
  72.       // Create the REGEX to text the URL with our desired parameters
  73.       const promoUrlQuery = new RegExp( '(&?)(' + queryParam + '=\\w+)', 'g' )
  74.  
  75.       // Check if the reqURL.search has these search query parameters
  76.       if(promoUrlQuery.test( reqURL.search )) {
  77.  
  78.         // The URL has promo query parameters that we need to remove
  79.         const urlSearchParams = reqURL.searchParams
  80.  
  81.         urlSearchParams.delete(queryParam)
  82.       }
  83.     } )
  84.  
  85.     return reqURL
  86.  
  87.   } catch (e) {
  88.     return event.respondWith(new Response(`Error thrown ${e.message}`));
  89.   }
  90. }
  91.  
  92. /**
  93.  * Function to check if the current request should be BYPASSed or Cached based on exclusion cookies
  94.  * entered by the user in the plugin settings
  95.  * @param {String} cookieHeader - The cookie header of the current request
  96.  * @param {Array} cookies_list - List of cookies which should not be cached
  97.  */
  98. function are_blacklisted_cookies(cookieHeader, cookies_list) {
  99.   let blackListedCookieExists = false
  100.  
  101.   // Make sure both cookieHeader & cookies_list are defined & the length of both cookieHeader & cookies_list > 0
  102.   if (
  103.     cookieHeader &&
  104.     cookieHeader.length > 0 &&
  105.     cookies_list &&
  106.     cookies_list.length > 0
  107.   ) {
  108.     // Split the received request cookie header by semicolon to an Array
  109.     const cookies = cookieHeader.split(';');
  110.  
  111.     // Loop through the cookies in the request header and check if there is any cookie present there
  112.     // which is also mentioned in our bypassed cookies array
  113.     // if there is then set blackListedCookieExists as true and break out of the loops
  114.     cookies.every((cookie) => {
  115.  
  116.       cookies_list.every((single_black_list_cookie) => {
  117.         if (cookie.trim().includes(single_black_list_cookie.trim())) {
  118.           blackListedCookieExists = true
  119.           // Found item. Break out from the loop
  120.           return false
  121.         }
  122.  
  123.         // Otherwise continue the loop
  124.         return true
  125.       })
  126.  
  127.       // Check if blackListedCookieExists is true then break out of this loop. Else continue the loop
  128.       return blackListedCookieExists ? false : true
  129.     })
  130.   }
  131.  
  132.   return blackListedCookieExists // value -> FALSE
  133. }
  134.  
  135. /**
  136.  * Function to add extra response headers for BYPASSed Requests
  137.  * @param {Response} res - The response object
  138.  * @param {String} reason - The string that hold the bypass reason
  139.  */
  140. function add_bypass_custom_headers(res, reason) {
  141.   if (res && (reason.length > 0)) {
  142.     // BYPASS the request and add our custom headers
  143.     res.headers.set('x-wp-cf-super-cache-worker-status', 'bypass')
  144.     res.headers.set('x-wp-cf-super-cache-worker-bypass-reason', reason)
  145.     res.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
  146.   }
  147. }
  148.  
  149. /**
  150.  * The function that handles the Request
  151.  * @param {Object} event
  152.  */
  153. async function handleRequest(event) {
  154.  
  155.   const request = event.request
  156.   const requestURL = url_normalize(event)
  157.   let response = false
  158.   let bypassCache = false
  159.   const bypassReason = {
  160.     'req_method': false,
  161.     'admin_req': false,
  162.     'file_path_ext': false,
  163.     'page_excluded': false,
  164.     'file_excluded': false,
  165.     'cookie': false
  166.   }
  167.   let bypassReasonDetails = ''
  168.   const cookieHeader = request.headers.get('cookie')
  169.   const reqDetails = {
  170.     'contentTypeHTML': false
  171.   }
  172.  
  173.   // ---------------------------------------------------------
  174.   // Check - Bypass Request ? - Only Based on Request Headers
  175.   // ---------------------------------------------------------
  176.  
  177.   // 1. BYPASS any requests whose request method is not GET or HEAD
  178.   const allowedReqMethods = ['GET', 'HEAD']
  179.   if (!bypassCache && request) {
  180.     if (!allowedReqMethods.includes(request.method)) {
  181.       bypassCache = true
  182.       bypassReason.req_method = true
  183.       bypassReasonDetails = `Caching not possible for req method ${request.method}`
  184.     }
  185.   }
  186.  
  187.   // 2. BYPASS the cache for WP Admin HTML Requests & Any File That has /wp-admin/ in it & API endpoints
  188.   // Get the Accept header of the request being received by the CF Worker
  189.   const accept = request.headers.get('Accept')
  190.  
  191.   if (!bypassCache && accept) {
  192.  
  193.     // List of path regex that we will BYPASS caching
  194.     // Path includes - WP Admin Paths, WP REST API, WooCommerce API, EDD API Endpoints
  195.     const bypass_admin_path = new RegExp(/(\/(wp-admin)(\/?))/g)
  196.     const bypass_cache_paths = new RegExp(/(\/((wp-admin)|(wc-api)|(edd-api))(\/?))/g)
  197.  
  198.     // List of file extensions to be BYPASSed
  199.     const bypass_file_ext = new RegExp(/\.(xsl|xml)$/)
  200.  
  201.     // Check if the request is for WP Admin endpoint & accept type includes text/html i.e. the main HTML request
  202.     if ( accept.includes('text/html') ) {
  203.       reqDetails.contentTypeHTML = true
  204.     }
  205.  
  206.     // Check if the request URL is an admin URL for HTML type requests
  207.     if ( reqDetails.contentTypeHTML && bypass_admin_path.test(requestURL.pathname) ) {
  208.       bypassCache = true
  209.       bypassReason.admin_req = true
  210.       bypassReasonDetails = 'WP Admin HTML request'
  211.  
  212.     } else if ( bypass_cache_paths.test(requestURL.pathname) || bypass_file_ext.test(requestURL.pathname) ) {
  213.       // This is for files which starts with /wp-admin/ but not supposed to be cached
  214.       // E.g. /wp-admin/load-styles.php || /wp-admin/admin-ajax.php
  215.       // Also API endpoints and xml/xsl files to ensure sitemap isn't cached
  216.  
  217.       bypassCache = true
  218.       bypassReason.file_path_ext = true
  219.       bypassReasonDetails = 'Dynamic File'
  220.     }
  221.   }
  222.  
  223.   // 3. BYPASS the cache if DEFAULT_BYPASS_COOKIES is present in the request
  224.   // AND also only for the HTML type requests
  225.   if (
  226.     !bypassCache &&
  227.     reqDetails.contentTypeHTML &&
  228.     cookieHeader &&
  229.     cookieHeader.length > 0 &&
  230.     DEFAULT_BYPASS_COOKIES.length > 0
  231.   ) {
  232.  
  233.     // Separate the request cookies by semicolon and create an Array
  234.     const cookies = cookieHeader.split(';')
  235.  
  236.     // Loop through the cookies Array to see if there is any cookies present that is present in DEFAULT_BYPASS_COOKIES
  237.     let foundDefaultBypassCookie = false
  238.  
  239.     cookies.every((cookie) => {
  240.  
  241.       DEFAULT_BYPASS_COOKIES.every((cookie_prefix) => {
  242.  
  243.         if (cookie.trim().startsWith(cookie_prefix.trim())) {
  244.           bypassCache = true
  245.           bypassReason.cookie = true
  246.           bypassReasonDetails = 'Default Bypass Cookie Present'
  247.           foundDefaultBypassCookie = true
  248.  
  249.           // Stop the loop
  250.           return false
  251.         }
  252.  
  253.         // Otherwise continue the loop
  254.         return true
  255.       })
  256.  
  257.       // Stop the loop if foundDefaultBypassCookie is TRUE else continue
  258.       return foundDefaultBypassCookie ? false : true
  259.     })
  260.   }
  261.  
  262.   /**
  263.    * Check if the Request has been Bypassed so far.
  264.    * If not, then check if the request exists in CF Edge Cache & if it does, send it
  265.    * If it does not exists in CF Edge Cache, then check if the request needs to be Bypassed based on the headers
  266.    * present in the Response.
  267.    */
  268.   if (!bypassCache) { // bypassCache is still FALSE
  269.  
  270.     // Check if the Request present in the CF Edge Cache
  271.     const cacheKey = new Request(requestURL, request)
  272.     const cache = caches.default // Get global CF cache object for this zone
  273.  
  274.     // Try to Get this request from this zone's cache
  275.     response = await cache.match(cacheKey)
  276.  
  277.     if (response) { // Cache is present for this request in the CF Edge. Nothing special needs to be done.
  278.  
  279.       // This request is already cached in the CF Edge. So, simply create a response and set custom headers
  280.       response = new Response(response.body, response)
  281.       response.headers.set('x-wp-cf-super-cache-worker-status', 'hit')
  282.  
  283.     } else { // Cache not present in CF Edge. Check if Req needs to be Bypassed or Cached based on Response header data
  284.  
  285.       // Fetch the response of this given request normally without any special parameters
  286.       // so that we can use the response headers set by the plugin at the server level
  287.       const fetchedResponse = await fetch(request)
  288.       response = new Response(fetchedResponse.body, fetchedResponse)
  289.  
  290.       // ---------------------------------------------------------
  291.       // Check - Bypass Request ? - Based on RESPONSE Headers
  292.       // ---------------------------------------------------------
  293.  
  294.       // 4. BYPASS the HTML page requests which are excluded from caching (via WP Admin plugin settings or page level settings)
  295.       if (
  296.         !bypassCache &&
  297.         response.headers.get('content-type').includes('text/html') &&
  298.         !response.headers.has('x-wp-cf-super-cache-active')
  299.       ) {
  300.         bypassCache = true
  301.         bypassReason.page_excluded = true
  302.         bypassReasonDetails = 'This page is excluded from caching'
  303.       }
  304.  
  305.       // 5. BYPASS the static files (non HTML) which has x-wp-cf-super-cache response header set to no-cache
  306.       if (!bypassCache &&
  307.         !response.headers.get('content-type').includes('text/html') &&
  308.         (response.headers.get('x-wp-cf-super-cache') === 'no-cache')
  309.       ) {
  310.         bypassCache = true
  311.         bypassReason.file_excluded = true
  312.         bypassReasonDetails = 'This file is excluded from caching'
  313.       }
  314.  
  315.       // 6. BYPASS cache if any custom cookie mentioned by the user in the plugin settings is present in the request
  316.       // Check only for HTML type requests
  317.       if (
  318.         !bypassCache &&
  319.         cookieHeader &&
  320.         cookieHeader.length > 0 &&
  321.         response &&
  322.         response.headers.get('content-type').includes('text/html') &&
  323.         response.headers.has('x-wp-cf-super-cache-cookies-bypass')
  324.       ) {
  325.         // Make sure the feature is enabled first
  326.         if (response.headers.get('x-wp-cf-super-cache-cookies-bypass') !== 'swfpc-feature-not-enabled') {
  327.  
  328.           // Get the list of cookie names entered by the user in the plugin settings
  329.           let cookies_blacklist = response.headers.get('x-wp-cf-super-cache-cookies-bypass');
  330.  
  331.           if (cookies_blacklist && cookies_blacklist.length > 0) {
  332.  
  333.             // Split the received cookie list with | separated and make an Array
  334.             cookies_blacklist = cookies_blacklist.split('|')
  335.  
  336.             if (are_blacklisted_cookies(cookieHeader, cookies_blacklist)) {
  337.               bypassCache = true
  338.               bypassReason.cookie = true
  339.               bypassReasonDetails = 'User provided excluded cookies present in request'
  340.             }
  341.           }
  342.         }
  343.       }
  344.  
  345.       //-----------------------------------------------------
  346.       // Check if the request needs to be BYPASSed or Cached
  347.       //-----------------------------------------------------
  348.       if (!bypassCache) { // bypassCache is still FALSE. Cache the item in the CF Edge
  349.  
  350.         // Check if the response status code is not 206 to cache using cache.put(), as any request with status code === 206
  351.         // cache.put() will not work. More info: https://developers.cloudflare.com/workers/runtime-apis/cache#put
  352.         if (response.status !== 206) {
  353.  
  354.           // If the response header has x-wp-cf-super-cache-active overwrite the cache-control header provided by the server value with x-wp-cf-super-cache-active value just to be safe
  355.           if (response.headers.has('x-wp-cf-super-cache-active')) {
  356.             response.headers.set('Cache-Control', response.headers.get('x-wp-cf-super-cache-cache-control'))
  357.           }
  358.  
  359.           // Set the worker status as miss and put the item in CF cache
  360.           response.headers.set('x-wp-cf-super-cache-worker-status', 'miss')
  361.  
  362.           // Add page in cache using cache.put()
  363.           event.waitUntil(cache.put(cacheKey, response.clone()))
  364.  
  365.         } else {
  366.  
  367.           // Try to fetch this request again with cacheEverything set to TRUE as that is the only way to cache it
  368.           // More info: https://developers.cloudflare.com/workers/runtime-apis/request#requestinitcfproperties
  369.           response = await fetch(request, { cf: { cacheEverything: true } })
  370.           response = new Response(response.body, response)
  371.  
  372.           // Set the worker status as miss and put the item in CF cache
  373.           response.headers.set('x-wp-cf-super-cache-worker-status', 'miss')
  374.  
  375.         }
  376.       } else { // bypassCache -> TRUE || Bypass the Request
  377.  
  378.         // BYPASS the request and add our custom headers
  379.         add_bypass_custom_headers(response, bypassReasonDetails)
  380.       }
  381.  
  382.     }
  383.  
  384.   } else { // bypassCache -> TRUE
  385.  
  386.     // Fetch the request from the origin server and send it by adding our custom bypass headers
  387.     const bypassedResponse = await fetch(request)
  388.     response = new Response(bypassedResponse.body, bypassedResponse)
  389.  
  390.     // BYPASS the request and add our custom headers
  391.     add_bypass_custom_headers(response, bypassReasonDetails)
  392.   }
  393.  
  394.   return response
  395. }
  396.  
  397. /**
  398.  * Adding event lister to the fetch event to catch the requests and manage them accordingly
  399.  * @param {Object} event
  400.  */
  401. addEventListener('fetch', event => {
  402.   try {
  403.     return event.respondWith(handleRequest(event));
  404.   } catch (e) {
  405.     return event.respondWith(new Response(`Error thrown ${e.message}`));
  406.   }
  407. })
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement