Introduction:

020 Short Link, a simple and free short link platform implemented using Cloudflare Worker

Deployment Method:

Step One: Create a Namespace

Go to the Workers & Pages section under the KV project

Create a namespace by clicking Create a namespace

Remember the name of this space you've created, you'll need it later

Step Two: Create a Worker

Go to the Workers & Pages section under the Overview project

Click Create application

Click Create Worker

Click deploy

Step Three: Configure Worker

Click Configure Worker

Go to the Settings section under the Variables project

Bind KV Namespace

Enter LINKS for Variable name, and fill in the namespace name you just created for KV namespace

Modify the domain in Triggers

Like this:

Click Quick edit at the top of the page

Enter the following content

// project name, which determines which project the html is fetched from.
const github_repo = typeof(GITHUB_REPO)!="undefined" ? GITHUB_REPO
    : 'AoEiuV020/Url-Shorten-Worker'
// Project version, cdn will have a cache, so you need to specify the version when there is an update.
const github_version = typeof(GITHUB_VERSION)!="undefined" ? GITHUB_VERSION
    : '@main'
// password, ignoring whitelist and timeout settings if the password is correct, and supporting customized short links.
const password = typeof(PASSWORD)!="undefined" ? PASSWORD
    : 'AoEiuV020 yes'
// Short chain timeout, in milliseconds, integer multiplication supported, 0 means no timeout set.
const shorten_timeout = typeof(SHORTEN_TIMEOUT)!="undefined" ? SHORTEN_TIMEOUT.split("*").reduce((a,b)=>parseInt(a)*parseInt(b),1)
    : (1000 * 60 * 10)
// The length of the default short chain key, which is automatically extended when duplicates are encountered.
const default_len = typeof(DEFAULT_LEN)!="undefined" ? parseInt(DEFAULT_LEN)
    : 6
// is true to enable demo, otherwise no password and non-whitelisted requests are not entertained, yes to allow visitors to try and expire after timeout.
const demo_mode = typeof(DEMO_MODE)!="undefined" ? DEMO_MODE === 'true'
    : true
// for true to automatically delete timed out demo short link records, otherwise just mark them as expired so that the history can be queried in the backend.
const remove_completely = typeof(REMOVE_COMPLETELY)!="undefined" ? REMOVE_COMPLETELY === 'true'
    : true
// Whitelisted domains ignore timeouts, json array format, just write the top level domain, automatically pass the top level domain and all second level domains
const white_list = JSON.parse(typeof(WHITE_LIST)!="undefined" ? WHITE_LIST
    : `[
"aoeiuv020.com",
"aoeiuv020.cn",
"aoeiuv020.cc",
"020.name"
    ]`)
//Displaying this no-abuse notice on a web page when demo mode is on doesn't need to make it clear when it expires.
const demo_notice = typeof(DEMO_NOTICE)!="undefined" ? DEMO_NOTICE
    : `Note: In order to prevent the example service from being abused, all links generated by the demo site may expire at any time, if you need to use it for a long time, please build it by yourself.`
//console.log(`${github_repo}, ${github_version}, ${password}, ${shorten_timeout}, ${demo_mode}, ${white_list}, ${demo_notice}`)
const html404 = `<!DOCTYPE html>
<body>
  <h1>404 Not Found.</h1>
  <p>The url you visit is not found.</p>
</body>`


async function randomString(len) {
    let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    /****Confusing characters are removed by default oOLl,9gq,Vv,Uu,I1****/
    let maxPos = $chars.length;
  let result = '';
  for (i = 0; i < len; i++) {
    result += $chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return result;
}
async function checkURL(url){
    let str=url;
    let Expression=/^http(s)?:\/\/(.*@)?([\w-]+\.)*[\w-]+([_\-.,~!*:#()\w\/?%&=]*)?$/;
    let objExp=new RegExp(Expression);
    if(objExp.test(str)==true){
      if (str[0] == 'h')
        return true;
      else
        return false;
    }else{
        return false;
    }
} 
// Check if the domain name is in the whitelist, the parameter contains only the domain name part
async function checkWhite(host){
    return white_list.some((h) => host == h || host.endsWith('.'+h))
} 
async function md5(message) {
  const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array
  const hashBuffer = await crypto.subtle.digest('MD5', msgUint8) // hash the message
  const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string

  return hashHex
}
async function checkHash(url, hash) {
    if (!hash) {
        return false
    }
    return (await md5(url+password)) == hash
}
async function save_url(url, key, admin, len) {
  len = len || default_len;
    // If the password is correct and the key is specified, the old value is overwritten
    const override = admin && key
    if (!override) {
        // Ignore the specified key if the password is incorrect
        key = await randomString(len)
    }
    const is_exists = await load_url(key)
    console.log("key exists " + key + " " + is_exists)
    if (override || !is_exists) {
        var mode = 3
        if (admin) {
            mode = 0
        }
        let value = `${mode};${Date.now()};${url}`
        if (remove_completely && mode != 0 && !await checkWhite(new URL(url).host)) {
          // Use expirationTtl to implement automatic deletion of expired records, below 60 seconds will report an error.
          let ttl = Math.max(60, shorten_timeout / 1000)
          console.log("key auto remove: " + key + ", " + ttl + "s")
          return await LINKS.put(key, value, {expirationTtl: ttl}),key
        } else {
          return await LINKS.put(key, value),key
        }
    } else {
        return await save_url(url, key, admin, len + 1)
    }
}
async function load_url(key) {
    const value = await LINKS.get(key)
    if (!value) {
        return null
    }
    const list = value.split(';')
    console.log("value split " + list)
    var url
    if (list.length == 1) {
        // The old data jumped forward normally for the time being
        url = list[0]
    } else {
        url = list[2]
        const mode = parseInt(list[0])
        const create_time = parseInt(list[1])
        if (mode != 0 && shorten_timeout > 0
            && Date.now() - create_time > shorten_timeout) {
            const host = new URL(url).host
            if (await checkWhite(host)) {
                console.log('white list')
            } else {
                // Timeouts and failures to find are handled in the same way
                console.log("shorten timeout")
                return null
            }
        }
    }
    return url
}
async function handleRequest(request) {
  console.log(request)
  if (request.method === "POST") {
    let req=await request.json()
    console.log("url " + req["url"])
    let admin = await checkHash(req["url"], req["hash"])
    console.log("admin " + admin)
    if(!await checkURL(req["url"]) || (!admin && !demo_mode && !await checkWhite(new URL(req["url"]).host))){
    // In non-demo mode, non-whitelisted addresses are treated as if they were not legitimate
    return new Response(`{"status":500,"key":": Error: Url illegal."}`, {
      headers: {
      "content-type": "text/html;charset=UTF-8",
      "Access-Control-Allow-Origin":"*",
      "Access-Control-Allow-Methods": "POST",
      },
    })}
    let stat,random_key=await save_url(req["url"], req["key"], admin)
    console.log("stat " + stat)
    if (typeof(stat) == "undefined"){
      return new Response(`{"status":200,"key":"/`+random_key+`"}`, {
      headers: {
      "content-type": "text/html;charset=UTF-8",
      "Access-Control-Allow-Origin":"*",
      "Access-Control-Allow-Methods": "POST",
      },
    })
    }else{
      return new Response(`{"status":200,"key":": Error:Reach the KV write limitation."}`, {
      headers: {
      "content-type": "text/html;charset=UTF-8",
      "Access-Control-Allow-Origin":"*",
      "Access-Control-Allow-Methods": "POST",
      },
    })}
  }else if(request.method === "OPTIONS"){  
      return new Response(``, {
      headers: {
      "content-type": "text/html;charset=UTF-8",
      "Access-Control-Allow-Origin":"*",
      "Access-Control-Allow-Methods": "POST",
      },
    })

  }

  const requestURL = new URL(request.url)
  const path = requestURL.pathname.split("/")[1]
  console.log(path)
  if(!path){

    const html= await fetch(`https://cdn.jsdelivr.net/gh/${github_repo}${github_version}/index.html`)
    const text = (await html.text())
        .replaceAll("###GITHUB_REPO###", github_repo)
        .replaceAll("###GITHUB_VERSION###", github_version)
        .replaceAll("###DEMO_NOTICE###", demo_notice)
    
    return new Response(text, {
    headers: {
      "content-type": "text/html;charset=UTF-8",
    },
  })
  }
  const url = await load_url(path)
  if (!url) {
    // 404, not found or timeout
    console.log('not found')
    return new Response(html404, {
      headers: {
        "content-type": "text/html;charset=UTF-8",
      },
      status: 404
    })
  }
  return Response.redirect(url, 302)
}



addEventListener("fetch", async event => {
  event.respondWith(handleRequest(event.request))
})

You can modify the following environment variables, where the Key is the corresponding uppercase:

  1. Adjust Timeout Settings

Short links generated in demonstration mode are inaccessible after timeout, and timeout settings are invalid if the whitelist or password is correct. Modify the variable shorten_timeout at the beginning of the script, in milliseconds, 0 means no timeout setting.

  1. Adjust Whitelist

Domain names in the whitelist have short links that ignore timeouts. Modify the variable white_list at the beginning of the script. It's a JSON array, write the top-level domain, and automatically pass the top-level domain and all second-level domains.

  1. Turn Off Demonstration Mode

Only when the demonstration mode is enabled are visitors allowed to add non-whitelist addresses without a password. The short link will expire after a timeout. Modify the variable demo_mode at the beginning of the script, true to enable demonstration, false to deny requests without a password and non-whitelisted.

  1. Automatically Delete Demonstration Records

Whether to automatically delete the expired short link records in demonstration mode. Modify the variable remove_completely at the beginning of the script, true to automatically delete expired demonstration short link records, otherwise just mark them as expired to query the history in the backend.

  1. Change Password

There's a hidden input box on the web page to enter a password. If the password is correct, it ignores the whitelist and timeout settings and supports custom short links. Modify the variable password at the beginning of the script. This private information is recommended to be configured directly in the environment variables.

  1. Modify Short Link Length

Short link length is the randomly generated key, which is the length of the short link's path part. If the length is not enough, it may cause duplication, and it will be automatically extended if duplication occurs. Modify the variable default_len at the beginning of the script.

Related Links:

GitHub address: https://github.com/AoEiuV020/Url-Shorten-Worker
Demo: https://020.name

Tag:Configure, URL Shortening Platform, Lightweight

Original link:http://enblog.fuyiran.link/Technology/29.html

Copyright: All posts on this blog, unless otherwise stated, are published using theCC BY-NC-SA 4.0 license agreement. Please indicate the source for reprinting Fu Speaking (enblog.fuyiran.link)

Add a new comment.