Skip to content

Instantly share code, notes, and snippets.

@cantoni
Forked from izuolan/worker.js
Created February 12, 2022 21:13
Show Gist options
  • Save cantoni/2fde66804294f43d68f957c9d8cc9ac7 to your computer and use it in GitHub Desktop.
Save cantoni/2fde66804294f43d68f957c9d8cc9ac7 to your computer and use it in GitHub Desktop.
Custom domain for your Craft.do pages. Demo: https://note.zuolan.me and Tutorial: https://zuolan.me/craft_custom_domain_en
// Your domain name
const MY_DOMAIN = 'note.example.com'
// Website language
const LANG = 'en'
// Favicon url
const FAVICON_URL = 'https://example.com/favicon.ico'
// Your config page link
const CONFIG_URL = 'https://www.craft.do/s/XXXXXXXXX'
// Your Telegram Token and ID
const TG_TOKEN = ""
const TG_CHAT_ID = ""
// END
// Default function
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
// Fetch url
async function fetchAndApply(request) {
let url = new URL(request.url)
// Set upstream domain
url.host = 'www.craft.do'
let pathname = url.pathname
let response = null
const config_obj = await configParser()
// Automatically generate robots.txt and sitemap.xml
if (pathname === '/robots.txt') {
return new Response('Sitemap: https://' + MY_DOMAIN + '/sitemap.xml')
}
if (pathname === '/sitemap.xml') {
let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
for(var path in config_obj){
sitemap += '<url><loc>https://' + MY_DOMAIN + '/' + path + '</loc></url>'
}
sitemap += '</urlset>'
response = new Response(sitemap)
response.headers.set('content-type', 'application/xml')
return response
}
if (pathname === '/favicon.svg') {
response = new Response('<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" viewBox="0 0 100 100"><g fill="blue" transform="translate(0.000000,100) scale(0.080000,-0.080000)"><path d="M762 1203 c-6 -15 -13 -46 -17 -68 -4 -22 -13 -49 -20 -61 -15 -23 -122 -69 -257 -109 -49 -14 -88 -28 -88 -29 0 -2 33 -20 73 -40 49 -24 87 -36 115 -36 28 0 42 -4 42 -13 0 -34 -295 -517 -390 -639 -40 -52 -4 -28 86 56 49 46 105 109 124 141 19 31 64 98 100 148 77 108 125 186 173 283 20 39 46 78 59 86 13 8 69 34 126 58 107 45 118 57 110 111 -3 21 -10 25 -78 34 l-75 10 -5 45 c-5 42 -7 45 -36 48 -26 3 -33 -1 -42 -25z"/><path d="M754 616 c-40 -19 -88 -39 -108 -46 -43 -14 -45 -30 -7 -72 25 -28 33 -31 80 -30 39 1 54 -3 58 -15 7 -18 -30 -140 -58 -192 -36 -67 6 -93 135 -84 l86 6 0 -26 c0 -14 -4 -37 -10 -51 -5 -14 -8 -26 -6 -26 7 0 110 68 129 85 11 10 17 30 17 60 0 62 -22 70 -150 57 -52 -5 -98 -6 -103 -2 -4 3 3 31 16 61 13 30 32 78 42 108 10 30 28 70 41 89 26 38 30 63 14 93 -17 31 -91 25 -176 -15z"/></g></svg>')
response.headers.set('content-type', 'image/svg+xml')
return response
}
// Default path
if (pathname === '/') {
url.pathname = '/s/' + config_obj['index'].slice(23)
}
// Prohibit other Craft.do share pages
else if (pathname.startsWith('/s/')) {
url.pathname = "/404"
}
// Proxy index blocks pages
else if (pathname.startsWith('/b/')) {
url.pathname = '/s/' + config_obj['index'].slice(23) + pathname
}
// TODO: There is an unresolved issue here
// External pages url is troublesome to deal with.
else if (pathname.includes('/x/')) {
// url.pathname = '/s/' + config_obj['index'].slice(23) + pathname
return Response.redirect('https://' + MY_DOMAIN, 301)
}
// Proxy js
else if (pathname.startsWith('/share/') && pathname.endsWith('.js')) {
response = await fetch(url)
let body = await response.text()
// replace all js files domain
response = new Response(body.replace(/www.craft.do/g, MY_DOMAIN), response)
response.headers.set('Content-Type', 'application/x-javascript')
}
// Proxy images
else if (pathname.startsWith('/img/')) {
// Proxy api.craft.do, pages preview api
// This code can proxy "MY_DOMAIN/img/<pathname>" --> "api.craft.do/render/preview/<slug>"
url.host = 'api.craft.do'
let path_name = pathname.slice(5)
let img_slug = config_obj[path_name].slice(23)
url.pathname = pathname.replace(pathname, '/render/preview/' + img_slug)
// Cache images
const cacheImage = `https://${url.host}${url.pathname}`
let response = await fetch(url.href, {
cf: {
// Always cache this fetch regardless of content type
// for a max of 86400 seconds before revalidating the resource
cacheTtl: 86400,
cacheEverything: true,
//Enterprise only feature, see Cache API for other plans
cacheKey: cacheImage,
},
})
// Reconstruct the Response object to make its headers mutable.
response = new Response(response.body, response)
// Set cache control headers to cache on browser for 25 minutes
response.headers.set("Cache-Control", "max-age=1500")
return response
}
// Disable Craft log.
else if (pathname.startsWith('/api/log/')) {
return new Response('Disable loging.')
}
// Proxy comment API.
else if (pathname.startsWith('/api/') && (pathname.includes('submitAnon'))) {
const init = {
body: request.body,
method: 'PUT',
headers: {
"content-type": "application/json;charset=UTF-8",
},
}
const response = await fetch(url.href, init)
const resp_json = await response.json()
const resp_str = JSON.stringify(resp_json)
// Telegram notify
const comment_message = resp_json.comments[0].content
const craft_slug = pathname.split("/")[5]
const craft_url = 'https://www.craft.do/s/' + craft_slug
const comment_slug = findJsonKey(config_obj, craft_url)
const tg_message = 'Comment URL:\n'
+ 'https://' + MY_DOMAIN + '/' + comment_slug
+ '\n\n'
+ 'Comment Message:\n' + comment_message
await sendToTelegram(tg_message)
return new Response(resp_str)
}
else {
try {
let urlIndexSlug = null
if (pathname.startsWith('/en/') || pathname.startsWith('/p/') || pathname.startsWith('/page/')) {
urlIndexSlug = pathname.split("/")[1] + '/' + pathname.split("/")[2]
} else {
urlIndexSlug = pathname.split("/")[1]
}
let configPath = config_obj[urlIndexSlug].slice(23)
url.pathname = '/s/' + configPath
console.log(url.pathname)
if (typeof(configPath) == "undefined") { throw new Error('404 not found: ' + configPath) }
} catch (error) {
if (pathname.startsWith('/api/') || pathname.endsWith('.css') || pathname.endsWith('.webmanifest') || pathname.endsWith('.svg')) {
// nothing
} else {
url.pathname = '/404'
// return new Response(error.message)
}
}
}
class AttributeRewriter {
element(element) {
if (element.getAttribute('property') === 'og:url') {
element.setAttribute('content', 'https://' + MY_DOMAIN + pathname)
}
if (element.getAttribute('property') === 'og:image') {
if (pathname === '/') { pathname = '/index' }
element.setAttribute('content', 'https://' + MY_DOMAIN + '/img' + pathname)
}
if (element.getAttribute('name') === 'luki:api-endpoint') {
element.setAttribute('content', 'https://' + MY_DOMAIN + '/api/')
}
if (element.getAttribute('lang') === 'en') {
element.setAttribute('lang', LANG)
}
if (element.getAttribute('rel') === 'icon') {
element.setAttribute('href', FAVICON_URL)
}
if (element.getAttribute('rel') === 'apple-touch-icon') {
element.setAttribute('href', FAVICON_URL)
}
}
}
class RemoveElement {
element(element) {
element.remove()
}
}
async function rewriteHTML(res) {
res.headers.delete("Content-Security-Policy")
return new HTMLRewriter()
.on('body', new BodyRewriter())
.on('head', new HeadRewriter())
.on('html', new AttributeRewriter())
.on('meta', new AttributeRewriter())
.on('link', new AttributeRewriter())
.on('meta[name="robots"]', new RemoveElement()) // SEO
.on('head>style', new RemoveElement()) // Remove fonts
.on('script[src="https://www.craft.do/assets/js/analytics2.js"]', new RemoveElement()) // Delete analytics js
.transform(res)
}
let method = request.method
let request_headers = request.headers
let new_request_headers = new Headers(request_headers)
new_request_headers.set('Host', url.hostname)
new_request_headers.set('Referer', url.hostname)
let original_response = await fetch(url.href, {
method: method,
headers: new_request_headers
})
let response_headers = original_response.headers
let new_response_headers = new Headers(response_headers)
let status = original_response.status
response = new Response(original_response.body, {
status,
headers: new_response_headers
})
// If you want change anything in response.
let text = await response.text()
// Return modified response.
let modified_response = new Response(text, {
status: response.status,
statusText: response.statusText,
headers: response.headers
})
if (pathname.startsWith('/share/static/js/') && (pathname.includes('codehighlight'))) {
return modified_response
} else {
return rewriteHTML(modified_response)
}
}
async function configParser() {
// Delete string "https://www.craft.do/s/"
let config_slug = CONFIG_URL.slice(23)
const api_url = 'https://www.craft.do/api/share/' + config_slug
const init = {
headers: {
"content-type": "application/json;charset=UTF-8",
},
}
const config_response = await fetch(api_url, init) // Get www.craft.do/api/share/<slug> content.
const response_json = await config_response.json() // Convert the content to json format (string).
const content_json = response_json.blocks[1].content // Get the json data of the first block in the body.
const content_str = JSON.stringify(content_json) // Convert json to string.
// Handle escape characters.
const config_json = content_str.replace(/\\t/g, '').replace(/\\n/g, '').replace(/\\/g, '').replace('"{', '{').replace('}"', '}')
let config_obj = JSON.parse(config_json)
return config_obj
}
async function sendToTelegram(message) {
const tgUrl = "https://api.telegram.org/bot" + TG_TOKEN + "/sendMessage"
const init = {
method: 'POST',
headers: {
"content-type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
"chat_id": TG_CHAT_ID,
"text": message
})
}
await fetch(tgUrl, init)
// const response = await fetch(tgUrl, init)
// const resp_text = await response.text()
// return new Response(resp_text)
}
function findJsonKey(obj, value, compare = (a, b) => a === b) {
return Object.keys(obj).find(k => compare(obj[k], value))
}
class BodyRewriter {
element(element) {
// Append your html
element.append(`
`, {
html: true
})
}
}
class HeadRewriter {
element(element) {
element.append(`
<style>
/* Hide the Craft "Login in" button in comment board. */
.sc-CtfFt {
visibility: hidden;
}
.hGGlzy {
visibility: hidden;
}
</style>
`, {
html: true
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment