jdjwzx233

jdjwzx233

如期待奇迹发生,那唯有不停伸手去抓紧,去把握才行
bilibili
github
email

Building jsdelivr mirror (Upyun/nginx/cfwokers)

Introduction#

jsDelivr — a free, fast, and reliable CDN for JS and open-source projects. However, due to slow access speeds in China, along with the revocation of jsDelivr's filing and frequent DNS pollution, sites using jsDelivr often experience slow access issues. You may ask, why not use someone else's mirror site? As the saying goes, "If you want something done right, do it yourself." Building your own mirror site allows you to have better control over your data and services. Using third-party mirror sites comes with potential risks and uncertainties. First, you cannot be sure of the reliability and stability of third-party mirror sites; they may shut down or encounter failures at any time. This can severely impact your site and potentially lead to service interruptions. Second, third-party mirror sites may not update and synchronize your site content in a timely manner, causing visitors to miss out on the latest information. Finally, building your own mirror site allows you to customize and manage your site more flexibly, meeting specific needs and providing a better user experience. Therefore, it is recommended to consider building your own mirror site whenever feasible to ensure data security and reliability.

Method 1: Using Youpai Cloud to Build#

Advantages:#

  1. Youpai Cloud is a well-known enterprise-level cloud service provider in China, offering fast, stable, and easy-to-use services, making it very suitable for building mirror sites. Youpai Cloud has many discounts: after joining the Youpai Cloud Alliance (the site needs to be filed, and you must add the Youpai Cloud logo at the bottom of the site/application and link to the official website), you can receive 10GB of storage space and 15GB of CDN traffic per month (issued annually, credited to the account with a 67 yuan voucher); new users registering (click here to register) can also directly receive a 61 yuan voucher (valid for one year). With these discounts, you can build a mirror site at zero cost.

  2. The acceleration effect is significant (as shown in the image)

Before acceleration:
https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/blog_img_加速前_d41d8cd98f00b204e9800998ecf8427e.png

After acceleration:

https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/blog_img_加速后_d41d8cd98f00b204e9800998ecf8427e.png

Disadvantages:#

Requires filing!!!

Recommendation Index:⭐⭐⭐⭐⭐#

Setup Method:#

Step 1:#

Create a Youpai Cloud account (click here to register)

Step 2:#

Go to the CDN management interface in the console (https://console.upyun.com/services/cdn/), create a service, and then configure the service (specific parameters are as follows).

  1. Service Name: A unique identifier for the service, e.g., image-upyun-com, one service can bind multiple custom domains.

Notes
Service name is limited to 5-20 characters; must start with a lowercase English letter, only supports a combination of lowercase English letters, numbers, and hyphens.

  1. Acceleration Domain: Fill in the acceleration domain that needs to be configured.

Notes
The acceleration domain must be filed with the Ministry of Industry and Information Technology; the domain to be accelerated has not yet been configured on the Youpai Cloud CDN platform.

The acceleration domain needs to undergo domain ownership verification, and can only be added successfully after verification.

  1. Application Scenario: Choose full site acceleration here.
  2. Origin Protocol: Select protocol follow.
  3. Origin Certificate Verification: Remember!! Do not enable (it cannot be used if enabled)!!!
  4. Line Configuration: Fill in the origin address cdn.jsdelivr.net, do not change the port number.

Step 3#

  1. After the service is successfully created, the operation interface will prompt that the CDN acceleration service has been successfully created and will automatically redirect to the service's [Function Configuration] interface.
  2. In the function configuration interface, there are function configuration modules such as domain management, origin management, cache configuration, performance optimization, HTTPS, access control, image processing, etc. In the [Domain Management] module, you can bind multiple custom domains for the service. Please wait patiently for the domain configuration (about 10 minutes) and check whether the status of the domain is [Normal].
  3. You can view the CNAME address assigned to you by the CDN platform. At this time, you need to go to the DNS resolver of the domain to add a CNAME record for that domain. After the CNAME configuration takes effect, you can use the CDN service.

Step 4 (Optional, but can enhance experience)#

  1. Configure HTTPS: Click [HTTPS], click the management on the right side of "HTTPS Configuration" (as shown in the image) https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/image_d41d8cd98f00b204e9800998ecf8427e.png then you can choose to add your own certificate or directly use the Youpai Cloud registered certificate (click here to go to certificate management). After adding, open [HTTPS Access], it is not recommended to check [Force HTTPS Access].

  2. It is recommended to enable intelligent compression and page compression (as shown in the image)
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703354977231_d41d8cd98f00b204e9800998ecf8427e.png

  3. It is recommended to configure cache, browser cache, and segmented cache (as shown in the image)
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355090340_d41d8cd98f00b204e9800998ecf8427e.png

    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355115278_d41d8cd98f00b204e9800998ecf8427e.png

    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355198422_d41d8cd98f00b204e9800998ecf8427e.png

    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355224230_d41d8cd98f00b204e9800998ecf8427e.png

  4. In [Access Control], enable Referer anti-leech, WAF protection, HTTP request body size limit, and IP access restrictions (as shown in the image)
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355343968_d41d8cd98f00b204e9800998ecf8427e.png
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355382813_d41d8cd98f00b204e9800998ecf8427e.png
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355473637_d41d8cd98f00b204e9800998ecf8427e.png
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703355505400_d41d8cd98f00b204e9800998ecf8427e.png

    This configuration is for reference only; please configure according to actual conditions!

  5. You can also configure [Edge Rules] to achieve some special effects. (Not demonstrated here)

Method 2: Using Cloudflare Workers to Build#

Advantages:#

  1. Simple, fewer steps, wide acceleration range.
  2. No filing required, good acceleration effect (as shown in the image)
    Before acceleration: https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/blog_img_加速前_d41d8cd98f00b204e9800998ecf8427e.png
    After acceleration: https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/cfwokerjsd_d41d8cd98f00b204e9800998ecf8427e.png
  3. No cost, with a free quota every day (100,000 requests free per day).

Disadvantages:#

  1. Access may be slow in some regions.
  2. The free quota is limited, which may not be enough for high-traffic sites.
  3. You need a domain that is linked to Cloudflare.

Recommendation Index:⭐⭐⭐⭐#

Setup Method#

Step 1#

  1. In the Cloudflare management panel, go to [Workers and Pages] -> [Overview] -> [Create Application] -> [Create Worker] (as shown in the image)
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703396996483_d41d8cd98f00b204e9800998ecf8427e.png
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703397051051_d41d8cd98f00b204e9800998ecf8427e.png
  2. Fill in any name and click [Deploy] (as shown in the image)

https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703397134262_d41d8cd98f00b204e9800998ecf8427e.png

Step 2#

  1. On the completed creation page, click [Edit Code] (as shown in the image) https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703397288998_d41d8cd98f00b204e9800998ecf8427e.png
  2. Paste the following code directly into the code box:
'use strict'

/**
 * static files (404.html, sw.js, conf.js)
 */
const ASSET_URL = 'https://cdn.jsdelivr.net'

const JS_VER = 10
const MAX_RETRY = 1

/** @type {RequestInit} */
const PREFLIGHT_INIT = {
  status: 204,
  headers: new Headers({
    'access-control-allow-origin': '*',
    'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
    'access-control-max-age': '1728000',
  }),
}

/**
 * @param {any} body
 * @param {number} status
 * @param {Object<string, string>} headers
 */
function makeRes(body, status = 200, headers = {}) {
  headers['--ver'] = JS_VER
  headers['access-control-allow-origin'] = '*'
  return new Response(body, {status, headers})
}


/**
 * @param {string} urlStr 
 */
function newUrl(urlStr) {
  try {
    return new URL(urlStr)
  } catch (err) {
    return null
  }
}


addEventListener('fetch', e => {
  const ret = fetchHandler(e)
    .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
  e.respondWith(ret)
})


/**
 * @param {FetchEvent} e 
 */
async function fetchHandler(e) {
  const req = e.request
  const urlStr = req.url
  const urlObj = new URL(urlStr)
  const path = urlObj.href.substr(urlObj.origin.length)

  if (urlObj.protocol === 'http:') {
    urlObj.protocol = 'https:'
    return makeRes('', 301, {
      'strict-transport-security': 'max-age=99999999; includeSubDomains; preload',
      'location': urlObj.href,
    })
  }

  if (path.startsWith('/http/')) {
    return httpHandler(req, path.substr(6))
  }

  switch (path) {
  case '/http':
    return makeRes('Please update cfworker to the latest version!')
  case '/ws':
    return makeRes('not support', 400)
  case '/works':
    return makeRes('it works')
  default:
    // static files
    return fetch(ASSET_URL + path)
  }
}


/**
 * @param {Request} req
 * @param {string} pathname
 */
function httpHandler(req, pathname) {
  const reqHdrRaw = req.headers
  if (reqHdrRaw.has('x-jsproxy')) {
    return Response.error()
  }

  // preflight
  if (req.method === 'OPTIONS' &&
      reqHdrRaw.has('access-control-request-headers')
  ) {
    return new Response(null, PREFLIGHT_INIT)
  }

  let acehOld = false
  let rawSvr = ''
  let rawLen = ''
  let rawEtag = ''

  const reqHdrNew = new Headers(reqHdrRaw)
  reqHdrNew.set('x-jsproxy', '1')

  // The logic here is roughly the same as http-dec-req-hdr.lua
  // https://github.com/EtherDream/jsproxy/blob/master/lua/http-dec-req-hdr.lua
  const refer = reqHdrNew.get('referer')
  const query = refer.substr(refer.indexOf('?') + 1)
  if (!query) {
    return makeRes('missing params', 403)
  }
  const param = new URLSearchParams(query)

  for (const [k, v] of Object.entries(param)) {
    if (k.substr(0, 2) === '--') {
      // System information
      switch (k.substr(2)) {
      case 'aceh':
        acehOld = true
        break
      case 'raw-info':
        [rawSvr, rawLen, rawEtag] = v.split('|')
        break
      }
    } else {
      // Restore HTTP request headers
      if (v) {
        reqHdrNew.set(k, v)
      } else {
        reqHdrNew.delete(k)
      }
    }
  }
  if (!param.has('referer')) {
    reqHdrNew.delete('referer')
  }

  // cfworker will merge `//` in the path into `/`
  const urlStr = pathname.replace(/^(https?):\/+/, '$1://')
  const urlObj = newUrl(urlStr)
  if (!urlObj) {
    return makeRes('invalid proxy url: ' + urlStr, 403)
  }

  /** @type {RequestInit} */
  const reqInit = {
    method: req.method,
    headers: reqHdrNew,
    redirect: 'manual',
  }
  if (req.method === 'POST') {
    reqInit.body = req.body
  }
  return proxy(urlObj, reqInit, acehOld, rawLen, 0)
}


/**
 * 
 * @param {URL} urlObj 
 * @param {RequestInit} reqInit 
 * @param {number} retryTimes 
 */
async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
  const res = await fetch(urlObj.href, reqInit)
  const resHdrOld = res.headers
  const resHdrNew = new Headers(resHdrOld)

  let expose = '*'
  
  for (const [k, v] of resHdrOld.entries()) {
    if (k === 'access-control-allow-origin' ||
        k === 'access-control-expose-headers' ||
        k === 'location' ||
        k === 'set-cookie'
    ) {
      const x = '--' + k
      resHdrNew.set(x, v)
      if (acehOld) {
        expose = expose + ',' + x
      }
      resHdrNew.delete(k)
    }
    else if (acehOld &&
      k !== 'cache-control' &&
      k !== 'content-language' &&
      k !== 'content-type' &&
      k !== 'expires' &&
      k !== 'last-modified' &&
      k !== 'pragma'
    ) {
      expose = expose + ',' + k
    }
  }

  if (acehOld) {
    expose = expose + ',--s'
    resHdrNew.set('--t', '1')
  }

  // verify
  if (rawLen) {
    const newLen = resHdrOld.get('content-length') || ''
    const badLen = (rawLen !== newLen)

    if (badLen) {
      if (retryTimes < MAX_RETRY) {
        urlObj = await parseYtVideoRedir(urlObj, newLen, res)
        if (urlObj) {
          return proxy(urlObj, reqInit, acehOld, rawLen, retryTimes + 1)
        }
      }
      return makeRes(res.body, 400, {
        '--error': `bad len: ${newLen}, except: ${rawLen}`,
        'access-control-expose-headers': '--error',
      })
    }

    if (retryTimes > 1) {
      resHdrNew.set('--retry', retryTimes)
    }
  }

  let status = res.status

  resHdrNew.set('access-control-expose-headers', expose)
  resHdrNew.set('access-control-allow-origin', '*')
  resHdrNew.set('--s', status)
  resHdrNew.set('--ver', JS_VER)

  resHdrNew.delete('content-security-policy')
  resHdrNew.delete('content-security-policy-report-only')
  resHdrNew.delete('clear-site-data')

  if (status === 301 ||
      status === 302 ||
      status === 303 ||
      status === 307 ||
      status === 308
  ) {
    status = status + 10
  }

  return new Response(res.body, {
    status,
    headers: resHdrNew,
  })
}


/**
 * @param {URL} urlObj 
 */
function isYtUrl(urlObj) {
  return (
    urlObj.host.endsWith('.googlevideo.com') &&
    urlObj.pathname.startsWith('/videoplayback')
  )
}

/**
 * @param {URL} urlObj 
 * @param {number} newLen 
 * @param {Response} res 
 */
async function parseYtVideoRedir(urlObj, newLen, res) {
  if (newLen > 2000) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  try {
    const data = await res.text()
    urlObj = new URL(data)
  } catch (err) {
    return null
  }
  if (!isYtUrl(urlObj)) {
    return null
  }
  return urlObj
}
  1. Click [Save and Deploy].

Step 3#

Return to [Overview], click on the project you just created, click [Triggers], click [Add Custom Domain], fill in a domain or subdomain from your account, click [Add Custom Domain], and complete the setup.

Method 3: Using nginx#

Advantages: Simple#

Disadvantages:#

The acceleration range is not wide, and the speed is limited by server performance and speed.

Recommendation Index:⭐⭐⭐#

Create a new site, then overwrite the following content in the nginx configuration file

server {

    listen 80;

    listen 443 ssl http2;

    # Please change to your certificate path
    ssl_certificate fullchain.pem;
    ssl_certificate_key privkey.pem;

    # Please change to your mirror domain
    server_name jsd.ucbk.cn;

    location / {
        proxy_pass https://cdn.jsdelivr.net;
        proxy_set_header Host $proxy_host;
        proxy_set_header Accept-Encoding '';
        proxy_ssl_server_name on;
        proxy_ssl_name $proxy_host;
        proxy_redirect / /;
        # Proxy Cache
        proxy_cache jsdelivr;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 15s;
        proxy_cache_use_stale updating;
        proxy_cache_background_update on;
        proxy_cache_key $host$request_uri;
        proxy_cache_valid 200 301 302 30d;
        proxy_cache_valid 500 501 502 503 15s;
        proxy_cache_valid any 5m;
        # Replace Domain
        sub_filter_once off;
        sub_filter_types application/javascript application/json text/xml text/css;
        sub_filter '$proxy_host' '$host';
    }

}
# Please change the cache path as needed
proxy_cache_path /var/tmp/nginx/jsdelivr levels=1:2 use_temp_path=off keys_zone=jsdelivr:300m inactive=30d max_size=30g;

Then directly change all instances of cdn.jsdelivr.net on your site to your own. If you don't want to change it, just add the following content to your site's nginx configuration.

sub_filter_once  off;
sub_filter_types application/javascript application/json text/xml text/css;
sub_filter 'cdn.jsdelivr.net'  'jsd.ucbk.cn';

Conclusion#

Thank you very much for reading this far. If you have any questions, feel free to raise them in the comments!

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.