jdjwzx233

jdjwzx233

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

搭建jsdelivr鏡像(又拍雲/nginx/cfwokers)

前言#

jsDelivr —— 一個免費、快速、可靠的為 JS 和開源項目服務的 CDN。但是由於國內訪問速度慢,加之 jsDelivr 備案被吊銷和 DNS 污染常出現,國內使用了 jsDelivr 的站點常常出現訪問速度慢的問題。
你可能會問,為什麼不去使用別人的鏡像站呢?俗話說 "自己動手,豐衣足食",自建鏡像站可以讓你更好地掌握自己的數據和服務。使用第三方鏡像站存在一些潛在的風險和不確定性。首先,你無法確定第三方鏡像站的可靠性和穩定性,它們可能在任何時候關閉或遭遇故障。這將對你的站點造成嚴重影響,可能導致服務中斷。其次,第三方鏡像站可能無法及時更新和同步你的站點內容,導致訪問者無法獲取最新的信息。最後,自建鏡像站可以讓你更加靈活地定制和管理自己的站點,滿足特定需求和提供更好的用戶體驗。因此,建議在可行的情況下,考慮自建鏡像站來確保數據的安全和可靠性。

方法一:使用又拍雲搭建#

優點:#

  1. 又拍雲是國內知名企業級雲服務商,其服務極速、穩定、簡單易用,用來搭建鏡像站是十分合適的。又拍雲有非常多的優惠:加入又拍雲聯盟 (網站需備案,要在網站 / 應用底部添加又拍雲 LOGO 並指向官網) 後,可以獲得每月 10G 儲存空間和 15G 的 CDN 流量 (按年發放,以 67 元的代金卷發放到账戶);新用戶註冊 (點我註冊) 還可以直接獲得 61 元的代金卷 (有效期一年)。在這些優惠的加成下,你可以零成本搭建一個鏡像站。

2. 加速效果明顯 (如圖)

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

加速後:

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

缺點:#

需要備案!!!

推薦指數:⭐⭐⭐⭐⭐#

搭建方法:#

第一步:#

創建又拍雲賬戶 (點我註冊)

第二步:#

進入控制台的 CDN 管理界面 (https://console.upyun.com/services/cdn/), 並創建服務,然後配置服務 (具體參數如下)。

  1. 服務名稱:唯一標識服務,例如:image-upyun-com,一個服務下面可以綁定多個自有域名。

注意事項
服務名稱僅限 5~20 位; 必須以小寫英文字符開頭,僅支持小寫英文字符、數字、中劃線組合。

  1. 加速域名:填寫此次需要配置的加速域名。

注意事項
加速域名必須已在工信部備案; 待加速域名尚未在又拍雲 CDN 平台配置。

加速域名需要進行域名所有權驗證,驗證通過後方能添加成功。

  1. 應用場景:這裡選擇全站加速
  2. 回源協議:選協議跟隨。
  3. 源站證書校驗:切記!!不要打開 (實測開了以後不能用)!!!
  4. 线路配置:源站地址填 cdn.jsdelivr.net, 端口號不要動。

第三步#

  1. 服務創建成功後,操作界面會提示 CDN 加速服務創建成功,並會自動跳轉到該服務的【功能配置】界面
  2. 在功能配置界面,有域名管理、回源管理、緩存配置、性能優化、HTTPS、訪問控制、圖片處理等功能配置模塊,在【域名管理】模塊下,可以針對該服務綁定多個自有域名,請耐心等待域名配置(約 10 分鐘),查看域名對應的狀態是否為[正常]
  3. 可查看 CDN 平台為您分配的 CNAME 地址,此時需要去域名 DNS 解析商處,為該域名添加一條 CNAME 記錄,待 CNAME 配置生效之後,方可使用 CDN 服務。

第四步 (非必要,但是可以提升體驗)#

  1. 配置 HTTPS: 點擊 [HTTPS], 點擊 "HTTPS 配置" 右邊的管理 (如圖)https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/image_d41d8cd98f00b204e9800998ecf8427e.png然後你可以選擇添加自有證書或直接使用又拍雲註冊證書 (點我前往證書管理)。添加完後,打開 [HTTPS 訪問],不建議勾選 [強制 HTTPS 訪問]。

  2. 建議打開智能壓縮和頁面壓縮 (如圖)
    https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703354977231_d41d8cd98f00b204e9800998ecf8427e.png

  3. 建議配置緩存、瀏覽器緩存和分段緩存 (如圖)
    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. 在 [訪問控制] 中,打開 Referer 防盜鏈、WAF 保護、HTTP 請求體大小限制、IP 訪問限制 (如圖)
    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

    此配置僅作參考,請根據實際情況進行配置!

  5. 你還可以配置 [邊緣規則],來實現一些特殊效果。(這裡不做演示)

方法二:使用 Cloudflare workers 搭建#

優點:#

  1. 簡單,步驟少,加速範圍廣
  2. 無需備案,加速效果較好 (如圖)
    加速前:https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/blog_img_加速前_d41d8cd98f00b204e9800998ecf8427e.png
    加速後:https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/cfwokerjsd_d41d8cd98f00b204e9800998ecf8427e.png
  3. 無費用,每天有免費額度 (每天免費 100000 個請求)

缺點:#

  1. 部分地區訪問較慢
  2. 免費額度較少,對於訪問量大的站點可能不夠
  3. 需要有一個掛在 cloudflare 的域名

推薦指數:⭐⭐⭐⭐#

搭建方法#

第一步#

  1. 在 Cloudflare 管理面板的 [Workers 和 Pages] 欄 ->[概述]->[創建應用程序]->[創建 Worker](如圖)
    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. 隨便填一個名稱,點擊 [部署](如圖)

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

第二步#

  1. 在創建完成的頁面點擊 [編輯代碼](如圖)https://upcdn.jdjwzx233.cn/upload/Qexo/23/12/24/1703397288998_d41d8cd98f00b204e9800998ecf8427e.png
  2. 將以下代碼直接粘貼進代碼框:
'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('請更新 cfworker 到最新版本!')
  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')

  // 此處邏輯和 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) === '--') {
      // 系統信息
      switch (k.substr(2)) {
      case 'aceh':
        acehOld = true
        break
      case 'raw-info':
        [rawSvr, rawLen, rawEtag] = v.split('|')
        break
      }
    } else {
      // 還原 HTTP 請求頭
      if (v) {
        reqHdrNew.set(k, v)
      } else {
        reqHdrNew.delete(k)
      }
    }
  }
  if (!param.has('referer')) {
    reqHdrNew.delete('referer')
  }

  // cfworker 會把路徑中的 `//` 合併成 `/`
  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
}

3. 點擊 [保存並部署]。

第三步#

回到 [概述],點擊你剛才創建的項目,點擊 [觸發器],點擊 [添加自定義域],填入一個你賬戶中的域名或其子域名,點擊 [添加自定義域],完成搭建。

方法三:使用 nginx#

優點:簡單#

缺點:#

加速範圍不廣,速度受到伺服器性能和速度限制。

推薦指數:⭐⭐⭐#

新建一個網站,然後把下面內容覆蓋到 nginx 配置文件

server {

    listen 80;

    listen 443 ssl http2;

    # 請更改為你的證書路徑
    ssl_certificate fullchain.pem;
    ssl_certificate_key privkey.pem;

    # 請更改為你的鏡像域名
    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';
    }

}
# 緩存路徑請根據需要更改
proxy_cache_path /var/tmp/nginx/jsdelivr levels=1:2 use_temp_path=off keys_zone=jsdelivr:300m inactive=30d max_size=30g;

之後直接把網站涉及到 cdn.jsdelivr.net都改成你自己的就可以了,如果不想改的話直接在你網站 nginx 配置中加以下內容就可以了

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

結尾#

非常感謝你能看到這裡,如有什麼問題歡迎在評論區中提出!

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。