緣起
上一篇文章中提到了利用自定義域名搭建可以在國內訪問的 Blogger 博客的方法,但經過討論和測試,這個方法存在以下幾個問題:
-
文章中的圖片等資源會被 Google 服務器自動緩存。對國外的用戶來說,這一機制可以大大提高頁面加載的速度,但對國內用戶來說則恰恰相反。目前除了在 Markdown 編輯器的預覽中直接復制富文本外還沒有別的解決方案。
-
地址 ghs.google.com 並不一定能解析到一個國內可以訪問的地址。雖然只要使用自定義域名就可以規避 DNS 污染和 SNI 阻斷,但是 Google 的服務器受到的是特殊待遇,直接針對 IP 地址進行阻斷,自定義域名也經常抽風。
反觀另一個同樣無法訪問的網站 Pixiv,網上給出的解決方案是直接在本地運行 Nginx 進行反代就可以訪問。這個解決方案本質上是利用了 SNI 協議的漏洞,即為了區分前往同一地址的不同域名,SNI 協議中會以明文展示你需要前往的域名。明文展示顯然會導致在特定的地區訪問不穩定等種種問題,所以互聯網領域的開發者們創造了 ESNI,后來又再 ESNI 的基礎上創造了 ECHO,然后是 ECH……然而根本問題是大多數網站根本就不打算去支持 ECH。為了解決在特定地區訪問不穩定的問題,給 SNI 加密固然是一個方法,但另一個更簡單粗暴的方法是:弄一個假的 SNI 或者根本不發 SNI!
(這里插播一個小故事:運營商有時會給官網等網站免流量,就是用 SNI 來判斷你訪問的是那個網站。曾經有人利用這個機制盜刷了價值數十萬元的流量,然后成功地被判刑了……)
總而言之,網絡上給出的 Pixiv 反代方案其實就是利用了 Nginx 反代時不支持 SNI,並且 Pixiv 並沒有直接被封鎖 IP,這就是為什么同樣的方案不能被用來訪問 Google,Blogger 使用自定義域名也偶爾抽風。
對於問題 2,套一層 CDN 看似是一個解決方案,但實踐表明這會導致包括但不限於循環重定向/HTTPS 證書錯誤/404 Not Found 等問題……
Cloudflare Workers
緣起
@PetrichorArk 提出可以用 CF Workers 反代來解決上述問題。簡單來說就是用一個 CF Workers 在訪問時爬取 Blogger 的頁面,將頁面中的資源進行替換后再反會給訪問者。(萬能的 Cloudflare Workers)
代碼實現
以下是 @PetrichorArk 編寫的實現代碼:
/**
* URL:
* https://something.something.workers.dev/
*/
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
const blogHost = 'something.blogspot.com'
/**
* @param {Map} error
* @param {Number} status
* @param {Boolean} cacheable
*/
function handleInvalidRequest(error, status, cacheable) {
const response = new Response(JSON.stringify(error), {
status: status,
statusText: 'Invalid Request',
headers: {
'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
'timing-allow-origin': '*',
'x-server': 'blog-proxy-2cff9aba',
'x-xss-protection': '1; mode=block'
}
})
if (cacheable) {
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
}
return response
}
/**
* @param {String} url
*/
async function fromCache(url) {
const cache = caches.default
const matched = await cache.match(url)
if (matched) {
return matched
}
const resp = await fetch(url)
if (resp.status >= 200 && resp.status < 300) {
const response = new Response(resp.body, {
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
})
response.headers.delete('expires')
response.headers.delete('vary')
response.headers.delete('access-control-allow-origin')
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
response.headers.set('strict-transport-security', 'max-age=31536000; includeSubDomains; preload')
response.headers.set('timing-allow-origin', '*')
response.headers.set('x-mirrored-url', url)
response.headers.set('x-server', 'blog-proxy-2cff9aba')
response.headers.set('x-xss-protection', '1; mode=block')
await cache.put(url, response.clone())
return response
}
return handleInvalidRequest({
msg: 'status_error',
url: url,
}, resp.status, false)
}
/**
* @param {URL} url
*/
async function proxy(url) {
const proxyHost = url.hostname
url.hostname = blogHost
const urlStr = url.href
const resp = await fetch(urlStr)
if (resp.status >= 200 && resp.status < 400) {
let body
const type = resp.headers.get('content-type')
if (type && type.startsWith('text/')) {
body = await resp.text()
body = body.replaceAll(blogHost, proxyHost)
body = body.replace(new RegExp(`<link href='(.*?)${proxyHost}/(.*?)' rel='canonical'/>`), `<link href='$1${blogHost}/$2' rel='canonical'/>`)
body = body.replace(/lh\w*?.googleusercontent.com/g, proxyHost + '/_image')
} else {
body = resp.body
}
const response = new Response(body, {
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
})
response.headers.delete('vary')
response.headers.delete('access-control-allow-origin')
response.headers.set('strict-transport-security', 'max-age=31536000; includeSubDomains; preload')
response.headers.set('timing-allow-origin', '*')
response.headers.set('x-mirrored-url', urlStr)
response.headers.set('x-server', 'blog-proxy-2cff9aba')
response.headers.set('x-xss-protection', '1; mode=block')
return response
}
return handleInvalidRequest({
msg: 'status_error',
url: urlStr,
}, resp.status, false)
}
/**
* @param {Request} request
*/
async function handleRequest(request) {
let url
try {
url = new URL(request.url)
} catch {
return handleInvalidRequest({ msg: 'url_parse_error', url: request.url }, 400, true)
}
if (url.pathname.startsWith('/_image/')) {
url.hostname = 'lh3.googleusercontent.com'
url.pathname = url.pathname.substr(7)
return await fromCache(url)
}
return await proxy(url)
}
使用時記得將代碼開頭處的 blogHost
賦值為你自己的 Blogger 的 Blogspot 域名。原先的域名要接觸與 Blogger 的綁定,這樣 CF Workers 才能訪問到你的真實博客頁面而不是一個重定向頁面。再將你的自定義域名解析到你的 CF Workers。(並在 Workers 面板設置路由。)
總結
Blogger 的地址 ghs.google.com
如果解析到國內可以訪問的 IP,訪問速度其實比 Cloudflare 的節點更快,但對於主題模版的修改的資源的處理則讓人頭大。相比之下,Cloudflare Workers 反代的方式更穩定也更便捷。不過折騰得這么麻煩為什么不直接用 Hexo,果然生命不止折騰不息嗎。