Guest User

Untitled

a guest
Sep 6th, 2023
119
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.85 KB | None | 0 0
  1. // CloudFlare worker for the domain Family
  2.  
  3. // bypass caching for those who are logged in
  4. const BYPASS_COOKIES = [
  5. 'betacentralauth_User',
  6. 'betacentralauth_Session'
  7. ];
  8.  
  9. const BYPASS_NAMESPACES = [ "Special", "Module" ];
  10.  
  11. // Internal parameters to strip from incoming requests, because they are used as part of the cache key.
  12. const CACHE_PARAMS = [
  13. 'tpCacheVer', // Cache breaker.
  14. 'tpMobile', // Mobile skin.
  15. ];
  16.  
  17. // cache breaker
  18. const CACHE_BREAKER = 0;
  19.  
  20. addEventListener('fetch', event => {
  21. console.log("start!:", Date.now())
  22.  
  23. // if we get an unknown error, skip the worker rather than throwing an error
  24. event.passThroughOnException();
  25.  
  26. const request = event.request;
  27. });
  28.  
  29. // process requests coming through
  30. async function processRequest(originalRequest, event) {
  31.  
  32. // prepare
  33. let {request, mobileAction, isMobile} = prepareRequest(originalRequest);
  34. let url = new URL(request.url)
  35.  
  36. // check if we should bypass, and why?
  37. let {status, bypassCache} = shouldBypass(request);
  38.  
  39. if ( !bypassCache) {
  40. // if we aren't bypassing, set the rules
  41. request = new Request(request, { cf: { cacheEverything: true } } );
  42. }
  43.  
  44. let alwaysOnline = false;
  45. // if the origin returns an error, lets try and respond from the cache
  46. const badStatus = response.status >=500 && response.status < 600
  47. console.log(badStatus, status)
  48. if ( badStatus && status == 'BYPASS COOKIE') {
  49. request = new Request(request, { cf: { cacheEverything: true} } );
  50. response = await fetch(request)
  51. alwaysOnline = true
  52. }
  53.  
  54. let cacheControl = '';
  55. cacheControl = response.headers.get('Cache-Control')
  56. // make the resppnses to origin modifiable
  57. response = new Response(response.body, response)
  58. if ( !bypassCache ) {
  59. response.headers.set('Cache-Control', 'private, must-revalidate, max-age=0, stale-while-revalidate=90');
  60. }
  61.  
  62. if ( status == "MISS" ) {
  63. status = response.headers.get('CF-Cache-Status')
  64. }
  65.  
  66. // handle the 'mobileaction' parameter cookie behaviour
  67. // Expire in one month, because 60*60*24*30 = 2592000.
  68. const expires = new Date(new Date().getTime() + 2592000000 ).toGMTString()
  69. if ( mobileAction == 'toggle_view_mobile' ) {
  70. response.headers.append('set-cookie', 'stopMobileRedirect=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.domain.net; secure')
  71. response.headers.append('set-cookie', 'mf_useformat=true; expires=' + expires + '; Max-Age=2592000; path=/; domain=.domain.net; secure')
  72. } else if ( mobileAction == 'toggle_view_desktop' ) {
  73. response.headers.append('set-cookie', 'stopMobileRedirect=true; expires=' + expires + '; Max-Age=2592000; path=/; domain=.domain.net; secure')
  74. 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')
  75. }
  76.  
  77. let ip = request.header.get('CF-Connecting-IP') || ''
  78.  
  79. // No more header manipulation possible past this point as the body will be streamed if neeeded.
  80.  
  81. // First, make sure this is HTML, otherwise bad things might happen if we start replacing random content.
  82. let contentType = response.headers.get('content-type')
  83.  
  84. if ( contentType && contentType.startsWith("text/html")) {
  85. let rewriter = new HTMLRewriter()
  86. // Lazy load all images on mobile.
  87. .on("img", {
  88. async element(node) {
  89. if( isMobile && !node.getAttribute('loading') ) {
  90. node.setAttribute('loading', 'lazy')
  91. }
  92. }
  93. })
  94. if (alwaysOnline) {
  95. rewriter.on("body.skin-vector", {
  96. async element(node) {
  97. 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})
  98. }
  99. })
  100. }
  101. }
  102. }
  103.  
  104. /**
  105. * Determine if the cache should be bypassed for the given cookies.
  106. * @param {Headers} headers - Headers
  107. * @returns {bool} true if the headers contain cookies that should bypass cache.
  108. */
  109.  
  110. function hasBypassCookies(headers) {
  111. let hasSession = false
  112. let hasUserID = false
  113. if (headers) {
  114. const cookieHeader = headers.get('cookie')
  115. if (cookieHeader && cookieHeader.length) {
  116. const cookies = cookieHeader.split(';')
  117. for (let cookie of cookies) {
  118. cookie = cookie.trim()
  119. if (cookie.startsWith('betacentralauth_Session')) {
  120. hasSession = true
  121. }
  122. else if (cookie.startsWith('wgToken')) {
  123. // wgToken should always bypass.
  124. return true
  125. }
  126. else if (cookie.startsWith('betacentralauth_User')) {
  127. hasUserID = true
  128. }
  129. }
  130. }
  131. }
  132.  
  133. // Sessions without user ID cookie are considered anonymous, so bypass if user ID cookie exists.
  134. if (hasSession && hasUserID) {
  135. return true
  136. }
  137.  
  138. // 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.
  139. return false
  140. }
  141.  
  142. /**
  143. * Determine if the cache should be bypassed for the given url path, based on namespace.
  144. * @param {string} path - path of url
  145. * @returns {bool} true if the url is in a namespace that should be bypassed.
  146. */
  147. function hasBypassNamespace(path) {
  148. for (let ns of BYPASS_NAMESPACES) {
  149. if (path.startsWith(`/wiki/${ns}`)) {
  150. return true
  151. }
  152. }
  153. return false
  154. }
  155.  
  156. /**
  157. * Check for cached HTML GET requests.
  158. *
  159. * @param {Request} request - Original request
  160. */
  161. function shouldBypass(request) {
  162. let bypassCache = false;
  163. let status = 'MISS';
  164.  
  165. const params = (new URL(request.url)).searchParams;
  166. const pathname = (new URL(request.url)).pathname;
  167.  
  168. // HTTP Method !== (GET || implicitly HEAD) bypasses cache.
  169. if (request.method !== 'GET') {
  170. bypassCache = true;
  171. status = 'BYPASS METHOD';
  172. } else if (hasBypassCookies(request.headers)) {
  173. bypassCache = true;
  174. status = 'BYPASS COOKIE';
  175. } else if (hasBypassNamespace(pathname)) {
  176. bypassCache = true;
  177. status = 'BYPASS NAMESPACE'
  178. }
  179. return {status, bypassCache};
  180. }
  181.  
  182. /**
  183. * Prepare every request coming through, normalise the request,
  184. * add the TP-Worker header, and the cache breaker.
  185. *
  186. * @param {Request} originalRequest - Original request
  187. */
  188. function prepareRequest(originalRequest) {
  189. // Clone the request.
  190. let request = new Request(originalRequest);
  191. // Add the TP-Worker header, so origin knows this is coming from the worker.
  192. let headers = new Headers(request.headers)
  193. headers.set('TP-Worker', '1');
  194. // Fix broken empty searches.
  195. let rurl = request.url.replace(/([?&])search=%s$/, '$1search=')
  196. // Just converting the URL to this object does a lot of normalisation for us,
  197. // like stripping empty query, fragment, and redundent port (i.e. :443 for https).
  198. let url = new URL(rurl)
  199. // Convert HEAD requests to GET requests so they can be cached.
  200. let method = request.method
  201. if (method == 'HEAD') {
  202. method = 'GET'
  203. }
  204. // Get the URL parameters for normalising.
  205. let params = new URLSearchParams(url.searchParams)
  206. const title = params.get('title')
  207. const pathname = url.pathname
  208. // Check for Special:Search request and normalise as needed.
  209. if ( method == 'GET' ) {
  210. // The '/' path is a special case as it takes the title parameter. Other paths ignore it.
  211. if (url.pathname == '/') {
  212. if (title !== null && title !== '') {
  213. url.pathname = '/wiki/' + title
  214. // Title has been determined at this point.
  215. params.delete('title')
  216. }
  217. }
  218. if ( url.pathname == '/wiki/' ) {
  219. url.pathname = '/'
  220. } else if ((url.pathname).startsWith('/wiki/')) {
  221. // Replace spaces in the title with underscores.
  222. url.pathname = (url.pathname).replaceAll('%20', '_')
  223. // MediaWiki converts consecutive spaces to one space.
  224. url.pathname = (url.pathname).replace(/_{2,}/g, '_')
  225. // MediaWiki strips leading and trailing spaces. Also strip trailing forward slashes.
  226. url.pathname = '/wiki/' + ((url.pathname).slice(3)).replace(/^_|_$|\/+$/g, '')
  227. // Rewrite first title character to uppercase.
  228. url.pathname = '/wiki/' + (url.pathname).charAt(3).toUpperCase() + (url.pathname).slice(4)
  229.  
  230. // If the first portion of the title is a namespace, then rewrite the first character after the namespace to uppercase.
  231. const namespaceEnd = (url.pathname).indexOf(':')
  232. if (namespaceEnd != -1 && ((url.pathname).length > (namespaceEnd + 1))) {
  233. url.pathname = (url.pathname).slice(0, namespaceEnd + 1) + (url.pathname).charAt(namespaceEnd + 1).toUpperCase() + (url.pathname).slice(namespaceEnd + 2)
  234. }
  235. }
  236. }
  237. // Normalise action parameters with regards to view being default and some aliases.
  238. const action = params.get('action')
  239. switch ( action ) {
  240. case 'editredlink':
  241. params.set('action', 'edit')
  242. break
  243. case 'unprotect':
  244. params.set('action', 'protect')
  245. break
  246. case 'view':
  247. params.delete('action')
  248. break
  249. default:
  250. // Do nothing.
  251. }
  252. // Delete internal cache-key parameters.
  253. for (p of CACHE_PARAMS) {
  254. params.delete(p)
  255. }
  256. // Handle mobile device detection.
  257. // Gather information from user-agent, cookies and parameters to determine which site should be shown.
  258. let isMobile = false
  259. const accept = request.headers.get('accept') || ''
  260. const cookie = request.headers.get('cookie') || ''
  261. const ua = request.headers.get('user-agent') || ''
  262. const mobileAction = params.get('mobileaction') // Also used in response to change cookies as appropriate.
  263. const forceMobile = mobileAction == 'toggle_view_mobile' || params.get('useformat') == 'mobile'
  264. const forceDesktop = mobileAction == 'toggle_view_desktop' || params.get('useformat') == 'desktop' || cookie.includes('stopMobileRedirect=true')
  265. const acceptsWml = accept.includes('vnd.wap.wml')
  266. const mobilePrefix = /^(lge?|sie|nec|sgh|pg)-/i.test(ua)
  267. 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)
  268. // Delete these parameters to avoid overriding the decision below.
  269. params.delete('mobileaction')
  270. params.delete('useformat')
  271. // Strip mf_useformat and stopMobileRedirect cookies from the request to avoid duplicate handling on the backend.
  272. let cookies = cookie
  273. if ( cookies.trim() !== '' ) {
  274. cookies = cookies.replace(/;?\s*mf_useformat=true/, '')
  275. cookies = cookies.replace(/;?\s*stopMobileRedirect=true/, '')
  276. }
  277. // Set the 'useformat' parameter to reflect which site should be shown.
  278. if ( forceMobile || ( !forceDesktop && ( cookie.includes('mf_useformat=true') || ( (acceptsWml || mobilePrefix || mobileUA) ) ) ) ) {
  279. isMobile = true
  280. if ( action !== 'raw' ) {
  281. params.set('tpMobile', '1')
  282. }
  283. }
  284. // Variants: Mobile, Desktop (Light, Dark, Light+Reader, Dark+Reader, Light+Sticky, Dark+Sticky)
  285. if (!isMobile) {
  286. cookies = 'stopMobileRedirect=true; ' + cookies;
  287. } else {
  288. cookies = 'mf_useformat=true; ' + cookies;
  289. }
  290. headers.set('cookie', cookies)
  291.  
  292. // The utm_ parameters are so numerous that regexing them is the best approach.
  293. for (let key of params.keys()) {
  294. if (/^utm_/.test(key)) {
  295. params.delete(key)
  296. }
  297. }
  298. // Add the cache breaker.
  299. if ( action !== 'raw' ) {
  300. params.set('tpCacheVer', CACHE_BREAKER);
  301. }
  302. // Sort the URL parameters to normalise the parameter order.
  303. // This implementation takes care of a=gff&a=gg and a[]=hh&a[]ff by preserving their relative order.
  304. params.sort()
  305.  
  306. // Set the final URL parameters.
  307. url.search = params
  308.  
  309. // Prevent recently logged-out users from receiving a 304, which would fool them into thinking they are still logged-in.
  310. if ( headers.has('if-modified-since') && cookie.includes('tpLoggedOut=') ) {
  311. headers.delete('if-modified-since')
  312. }
  313.  
  314. // Fix URL encoding to match what MediaWiki expects. See https://phabricator.wikimedia.org/T210295
  315. // Fortunately, all but '/'(%2F) are already always decoded.
  316. //const decode_set = [':', '/', '@', '!', '$', '(', ')', '*', ',', ';']
  317. const encode_set = ['[', ']', '&', '\'', '+', '=']
  318.  
  319. function encodeSet(match, offset, string) {
  320. return string[offset] = '%' + match.charCodeAt(0).toString(16).toUpperCase()
  321. }
  322.  
  323. const fixedPath = url.pathname.replace(/(?:[\[\]&'+=])/g, encodeSet).replace('%2F','/')
  324.  
  325. // TODO: Fix always decoded set as well.
  326. const fixedUrl = 'https://' + url.hostname + fixedPath + url.search
  327.  
  328. request = new Request(fixedUrl, new Request(request, { headers: headers, method: method }))
  329.  
  330. return { request, mobileAction, isMobile }
  331. }
Advertisement
Add Comment
Please, Sign In to add comment