Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // CloudFlare worker for the domain Family
- // bypass caching for those who are logged in
- const BYPASS_COOKIES = [
- 'betacentralauth_User',
- 'betacentralauth_Session'
- ];
- const BYPASS_NAMESPACES = [ "Special", "Module" ];
- // Internal parameters to strip from incoming requests, because they are used as part of the cache key.
- const CACHE_PARAMS = [
- 'tpCacheVer', // Cache breaker.
- 'tpMobile', // Mobile skin.
- ];
- // cache breaker
- const CACHE_BREAKER = 0;
- addEventListener('fetch', event => {
- console.log("start!:", Date.now())
- // if we get an unknown error, skip the worker rather than throwing an error
- event.passThroughOnException();
- const request = event.request;
- });
- // process requests coming through
- async function processRequest(originalRequest, event) {
- // prepare
- let {request, mobileAction, isMobile} = prepareRequest(originalRequest);
- let url = new URL(request.url)
- // check if we should bypass, and why?
- let {status, bypassCache} = shouldBypass(request);
- if ( !bypassCache) {
- // if we aren't bypassing, set the rules
- request = new Request(request, { cf: { cacheEverything: true } } );
- }
- let alwaysOnline = false;
- // if the origin returns an error, lets try and respond from the cache
- const badStatus = response.status >=500 && response.status < 600
- console.log(badStatus, status)
- if ( badStatus && status == 'BYPASS COOKIE') {
- request = new Request(request, { cf: { cacheEverything: true} } );
- response = await fetch(request)
- alwaysOnline = true
- }
- let cacheControl = '';
- cacheControl = response.headers.get('Cache-Control')
- // make the resppnses to origin modifiable
- response = new Response(response.body, response)
- if ( !bypassCache ) {
- response.headers.set('Cache-Control', 'private, must-revalidate, max-age=0, stale-while-revalidate=90');
- }
- if ( status == "MISS" ) {
- status = response.headers.get('CF-Cache-Status')
- }
- // handle the 'mobileaction' parameter cookie behaviour
- // Expire in one month, because 60*60*24*30 = 2592000.
- const expires = new Date(new Date().getTime() + 2592000000 ).toGMTString()
- if ( mobileAction == 'toggle_view_mobile' ) {
- response.headers.append('set-cookie', 'stopMobileRedirect=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.domain.net; secure')
- response.headers.append('set-cookie', 'mf_useformat=true; expires=' + expires + '; Max-Age=2592000; path=/; domain=.domain.net; secure')
- } else if ( mobileAction == 'toggle_view_desktop' ) {
- response.headers.append('set-cookie', 'stopMobileRedirect=true; expires=' + expires + '; Max-Age=2592000; path=/; domain=.domain.net; secure')
- response.headers.append('set-cookie', 'mf_useformat=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.domain.net; secure')
- }
- let ip = request.header.get('CF-Connecting-IP') || ''
- // No more header manipulation possible past this point as the body will be streamed if neeeded.
- // First, make sure this is HTML, otherwise bad things might happen if we start replacing random content.
- let contentType = response.headers.get('content-type')
- if ( contentType && contentType.startsWith("text/html")) {
- let rewriter = new HTMLRewriter()
- // Lazy load all images on mobile.
- .on("img", {
- async element(node) {
- if( isMobile && !node.getAttribute('loading') ) {
- node.setAttribute('loading', 'lazy')
- }
- }
- })
- if (alwaysOnline) {
- rewriter.on("body.skin-vector", {
- async element(node) {
- node.prepend('<div style="background-color: #ffdcdc;padding: 0.3em 0.5em;position:fixed;width:100%;border-top: 1px solid #c70000;z-index:5;font-size: .9em;font-weight: 700;bottom: 0;">The wiki servers are temporarily unavailable. Your request is being served from the cache.</div>', {html: true})
- }
- })
- }
- }
- }
- /**
- * Determine if the cache should be bypassed for the given cookies.
- * @param {Headers} headers - Headers
- * @returns {bool} true if the headers contain cookies that should bypass cache.
- */
- function hasBypassCookies(headers) {
- let hasSession = false
- let hasUserID = false
- if (headers) {
- const cookieHeader = headers.get('cookie')
- if (cookieHeader && cookieHeader.length) {
- const cookies = cookieHeader.split(';')
- for (let cookie of cookies) {
- cookie = cookie.trim()
- if (cookie.startsWith('betacentralauth_Session')) {
- hasSession = true
- }
- else if (cookie.startsWith('wgToken')) {
- // wgToken should always bypass.
- return true
- }
- else if (cookie.startsWith('betacentralauth_User')) {
- hasUserID = true
- }
- }
- }
- }
- // Sessions without user ID cookie are considered anonymous, so bypass if user ID cookie exists.
- if (hasSession && hasUserID) {
- return true
- }
- // At this point either no session exists or it is an anonymous session, if it is an anonymous session, the other bypass checks make this safe.
- return false
- }
- /**
- * Determine if the cache should be bypassed for the given url path, based on namespace.
- * @param {string} path - path of url
- * @returns {bool} true if the url is in a namespace that should be bypassed.
- */
- function hasBypassNamespace(path) {
- for (let ns of BYPASS_NAMESPACES) {
- if (path.startsWith(`/wiki/${ns}`)) {
- return true
- }
- }
- return false
- }
- /**
- * Check for cached HTML GET requests.
- *
- * @param {Request} request - Original request
- */
- function shouldBypass(request) {
- let bypassCache = false;
- let status = 'MISS';
- const params = (new URL(request.url)).searchParams;
- const pathname = (new URL(request.url)).pathname;
- // HTTP Method !== (GET || implicitly HEAD) bypasses cache.
- if (request.method !== 'GET') {
- bypassCache = true;
- status = 'BYPASS METHOD';
- } else if (hasBypassCookies(request.headers)) {
- bypassCache = true;
- status = 'BYPASS COOKIE';
- } else if (hasBypassNamespace(pathname)) {
- bypassCache = true;
- status = 'BYPASS NAMESPACE'
- }
- return {status, bypassCache};
- }
- /**
- * Prepare every request coming through, normalise the request,
- * add the TP-Worker header, and the cache breaker.
- *
- * @param {Request} originalRequest - Original request
- */
- function prepareRequest(originalRequest) {
- // Clone the request.
- let request = new Request(originalRequest);
- // Add the TP-Worker header, so origin knows this is coming from the worker.
- let headers = new Headers(request.headers)
- headers.set('TP-Worker', '1');
- // Fix broken empty searches.
- let rurl = request.url.replace(/([?&])search=%s$/, '$1search=')
- // Just converting the URL to this object does a lot of normalisation for us,
- // like stripping empty query, fragment, and redundent port (i.e. :443 for https).
- let url = new URL(rurl)
- // Convert HEAD requests to GET requests so they can be cached.
- let method = request.method
- if (method == 'HEAD') {
- method = 'GET'
- }
- // Get the URL parameters for normalising.
- let params = new URLSearchParams(url.searchParams)
- const title = params.get('title')
- const pathname = url.pathname
- // Check for Special:Search request and normalise as needed.
- if ( method == 'GET' ) {
- // The '/' path is a special case as it takes the title parameter. Other paths ignore it.
- if (url.pathname == '/') {
- if (title !== null && title !== '') {
- url.pathname = '/wiki/' + title
- // Title has been determined at this point.
- params.delete('title')
- }
- }
- if ( url.pathname == '/wiki/' ) {
- url.pathname = '/'
- } else if ((url.pathname).startsWith('/wiki/')) {
- // Replace spaces in the title with underscores.
- url.pathname = (url.pathname).replaceAll('%20', '_')
- // MediaWiki converts consecutive spaces to one space.
- url.pathname = (url.pathname).replace(/_{2,}/g, '_')
- // MediaWiki strips leading and trailing spaces. Also strip trailing forward slashes.
- url.pathname = '/wiki/' + ((url.pathname).slice(3)).replace(/^_|_$|\/+$/g, '')
- // Rewrite first title character to uppercase.
- url.pathname = '/wiki/' + (url.pathname).charAt(3).toUpperCase() + (url.pathname).slice(4)
- // If the first portion of the title is a namespace, then rewrite the first character after the namespace to uppercase.
- const namespaceEnd = (url.pathname).indexOf(':')
- if (namespaceEnd != -1 && ((url.pathname).length > (namespaceEnd + 1))) {
- url.pathname = (url.pathname).slice(0, namespaceEnd + 1) + (url.pathname).charAt(namespaceEnd + 1).toUpperCase() + (url.pathname).slice(namespaceEnd + 2)
- }
- }
- }
- // Normalise action parameters with regards to view being default and some aliases.
- const action = params.get('action')
- switch ( action ) {
- case 'editredlink':
- params.set('action', 'edit')
- break
- case 'unprotect':
- params.set('action', 'protect')
- break
- case 'view':
- params.delete('action')
- break
- default:
- // Do nothing.
- }
- // Delete internal cache-key parameters.
- for (p of CACHE_PARAMS) {
- params.delete(p)
- }
- // Handle mobile device detection.
- // Gather information from user-agent, cookies and parameters to determine which site should be shown.
- let isMobile = false
- const accept = request.headers.get('accept') || ''
- const cookie = request.headers.get('cookie') || ''
- const ua = request.headers.get('user-agent') || ''
- const mobileAction = params.get('mobileaction') // Also used in response to change cookies as appropriate.
- const forceMobile = mobileAction == 'toggle_view_mobile' || params.get('useformat') == 'mobile'
- const forceDesktop = mobileAction == 'toggle_view_desktop' || params.get('useformat') == 'desktop' || cookie.includes('stopMobileRedirect=true')
- const acceptsWml = accept.includes('vnd.wap.wml')
- const mobilePrefix = /^(lge?|sie|nec|sgh|pg)-/i.test(ua)
- const mobileUA = /mobi|240x240|240x320|320x320|alcatel|android|audiovox|bada|benq|blackberry|cdm-|compal-|docomo|ericsson|hiptop|htc[-_]|huawei|ipod|kddi-|kindle|meego|midp|mitsu|mmp\/|mot-|motor|ngm_|nintendo|opera.m|palm|panasonic|philips|phone|playstation|portalmmm|sagem-|samsung|sanyo|sec-|semc-browser|sendo|sharp|silk|softbank|symbian|teleca|up.browser|vodafone|webos/i.test(ua)
- // Delete these parameters to avoid overriding the decision below.
- params.delete('mobileaction')
- params.delete('useformat')
- // Strip mf_useformat and stopMobileRedirect cookies from the request to avoid duplicate handling on the backend.
- let cookies = cookie
- if ( cookies.trim() !== '' ) {
- cookies = cookies.replace(/;?\s*mf_useformat=true/, '')
- cookies = cookies.replace(/;?\s*stopMobileRedirect=true/, '')
- }
- // Set the 'useformat' parameter to reflect which site should be shown.
- if ( forceMobile || ( !forceDesktop && ( cookie.includes('mf_useformat=true') || ( (acceptsWml || mobilePrefix || mobileUA) ) ) ) ) {
- isMobile = true
- if ( action !== 'raw' ) {
- params.set('tpMobile', '1')
- }
- }
- // Variants: Mobile, Desktop (Light, Dark, Light+Reader, Dark+Reader, Light+Sticky, Dark+Sticky)
- if (!isMobile) {
- cookies = 'stopMobileRedirect=true; ' + cookies;
- } else {
- cookies = 'mf_useformat=true; ' + cookies;
- }
- headers.set('cookie', cookies)
- // The utm_ parameters are so numerous that regexing them is the best approach.
- for (let key of params.keys()) {
- if (/^utm_/.test(key)) {
- params.delete(key)
- }
- }
- // Add the cache breaker.
- if ( action !== 'raw' ) {
- params.set('tpCacheVer', CACHE_BREAKER);
- }
- // Sort the URL parameters to normalise the parameter order.
- // This implementation takes care of a=gff&a=gg and a[]=hh&a[]ff by preserving their relative order.
- params.sort()
- // Set the final URL parameters.
- url.search = params
- // Prevent recently logged-out users from receiving a 304, which would fool them into thinking they are still logged-in.
- if ( headers.has('if-modified-since') && cookie.includes('tpLoggedOut=') ) {
- headers.delete('if-modified-since')
- }
- // Fix URL encoding to match what MediaWiki expects. See https://phabricator.wikimedia.org/T210295
- // Fortunately, all but '/'(%2F) are already always decoded.
- //const decode_set = [':', '/', '@', '!', '$', '(', ')', '*', ',', ';']
- const encode_set = ['[', ']', '&', '\'', '+', '=']
- function encodeSet(match, offset, string) {
- return string[offset] = '%' + match.charCodeAt(0).toString(16).toUpperCase()
- }
- const fixedPath = url.pathname.replace(/(?:[\[\]&'+=])/g, encodeSet).replace('%2F','/')
- // TODO: Fix always decoded set as well.
- const fixedUrl = 'https://' + url.hostname + fixedPath + url.search
- request = new Request(fixedUrl, new Request(request, { headers: headers, method: method }))
- return { request, mobileAction, isMobile }
- }
Advertisement
Add Comment
Please, Sign In to add comment