From 44aac0fe673a83a83957dd2049715d73cc454f45 Mon Sep 17 00:00:00 2001 From: Erisa A Date: Thu, 18 Mar 2021 19:52:05 +0000 Subject: [PATCH] Reinitalize repository --- .github/FUNDING.yml | 1 + .gitignore | 11 +++ .prettierrc | 7 ++ LICENSE | 25 +++++ README.md | 78 ++++++++++++++++ index.js | 220 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 ++++ wrangler.toml | 11 +++ 8 files changed, 369 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 wrangler.toml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9f39ea6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Erisa diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3cd929 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target +/dist +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +worker/ +node_modules/ +.cargo-ok +wrangler.prod.toml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a06a385 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "all", + "tabWidth": 2, + "printWidth": 80 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f1a4e34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2021 Erisa A. + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ad35fd --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Worker Links (URL Shortener) + +A simple URL Shortener for Cloudflare Workers, using Workers KV. Redirect short URLs at the edge of the Cloudflare network to keep latency down and access fast! + +I run this code in production on [erisa.link](https://erisa.link/example), though the root name redirects and without the secret it doesn't make a very good demo. + +It was made for my personal use but is available publicly in the hopes that it may be useful to someone somewhere. + +## Usage + +To deploy to your Cloudflare Workers account, edit the relevant entries in `wrangler.toml`, add a secret with `wrangler put WORKERLINKS_SECRET` and use `wrangler publish`. +For debugging, you can use `wrangler preview`, though note you will need to login and configure a preview KV namespace in `wrangler.toml`. + +Once deployed, interacting with the API should be rather simple. It's based on headers, specifically with the `Authorization` and `URL` headers. + +To create a short URL with a random URL, send a `POST` to `/` with `Authorization` and `URL` headers: + +```json +erisa@Tuturu:~$ curl -X POST -H "Authorization: mysecret" -H "URL: https://erisa.moe" https://erisa.link/ +{ + "message": "URL created succesfully.", + "key": "q2w083eq", + "shorturl": "https://erisa.link/q2w083eq", + "longurl": "https://erisa.moe" +} +``` + +And you can test it worked if you wish: + +```http +erisa@Tuturu:~$ curl https://erisa.link/q2w083eq -D- +HTTP/2 302 +date: Fri, 11 Sep 2020 12:43:04 GMT +content-length: 0 +location: https://erisa.moe +server: cloudflare +..other ephemeral headers.. +``` + +To create or update a custom short URl, send a `PUT` to the intended target URL: + +```json +erisa@Tuturu:~$ curl -X PUT -H "Authorization: mysecret" -H "URL: https://erisa.moe" https://erisa.link/mywebsite +{ + "message": "URL created succesfully.", + "key": "mywebsite", + "shorturl": "https://erisa.link/mywebsite", + "longurl": "https://erisa.moe" +} +``` + +And to delete an existing shortlink, send a `DELETE` to it with only the `Authorization` header: + +```json +erisa@Tuturu:~$ curl -X DELETE -H "Authorization: mysecret" https://erisa.link/keytodelete +{ + "message": "Short URL deleted succesfully.", + "key": "keytodelete", + "shorturl": "https://erisa.link/keytodelete", + "longurl": "https://erisa.moe" +} +``` + +It is a planned feature to be able to list all URLs via a `GET` on `/` with `Authorization`. + +For the time being you can view them from your Cloudflare Dashboard: +Cloudflare Dashboard -> Workers -> KV -> View on the namespace. + +Or with the `wrangler` tool. +For example if you are in the project directory and have your `wrangler.toml` configured correctly, this should just be `wrangler kv:key list --binding kv` + +## Security + +This code is relatively simple but still, if you find any security issues that can be exploited publicly, please reach out to me via email: `seriel (at) erisa.moe` with any relevant details. + +If you don't have access to Workers KV you're welcome to test these issues on my live `erisa.link`, provided you don't send excessive (constant) requests or delete/modify any keys except ones created by you or the `/sample` key. + +If I don't respond to your email for whatever reason please feel free to publicly open an issue. diff --git a/index.js b/index.js new file mode 100644 index 0000000..9e0ef2e --- /dev/null +++ b/index.js @@ -0,0 +1,220 @@ +// Set this in your worker's environment. wrangler.toml or cloudflare dashboard. +let secret = WORKERLINKS_SECRET + +addEventListener('fetch', (event) => { + event.respondWith(handleRequest(event.request)) +}) + +/** + * Respond to the request + * @param {Request} request + */ +async function handleRequest(request) { + var key = new URL(request.url).pathname + var shorturl = new URL(request.url).origin + key + + if (request.method == 'PUT') { + return await putLink( + request.headers.get('Authorization'), + shorturl, + key, + request.headers.get('URL'), + ) + } else if (request.method == 'POST') { + if (key != '/') { + return new Response( + JSON.stringify({ + code: '405 Method Not Allowed', + message: 'POST not valid for individual keys. Did you mean PUT?', + }), + { + status: 405, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + key = '/' + Math.random().toString(36).slice(5) + shorturl = new URL(request.url).origin + key + return await putLink( + request.headers.get('Authorization'), + shorturl, + key, + request.headers.get('URL'), + ) + } else if (request.method == 'GET' || request.method == 'HEAD') { + let url = await kv.get(key) + if (url == null) { + return new Response( + JSON.stringify( + { + code: '404 Not Found', + message: 'Key does not exist or has not propagated.', + }, + null, + 2, + ), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } else { + return new Response(null, { status: 302, headers: { Location: url } }) + } + } else if (request.method == 'DELETE') { + if (request.headers.get('Authorization') != secret) { + return new Response( + JSON.stringify( + { + code: '401 Unauthorized', + message: 'Unauthorized.', + }, + null, + 2, + ), + { + status: 401, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + + shorturl = new URL(request.url).origin + key + let url = await kv.get(key) + if (url == null) { + return new Response( + JSON.stringify( + { + code: '404 Not Found', + message: 'Key does not exist or has not propagated.', + }, + null, + 2, + ), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } else { + kv.delete(key) + return new Response( + JSON.stringify( + { + message: 'Short URL deleted succesfully.', + key: key.substr(1), + shorturl: shorturl, + longurl: url, + }, + null, + 2, + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + } + + return new Response( + JSON.stringify( + { + code: '405 Method Not Allowed', + message: + 'Unsupported method. Please use one of GET, PUT, POST, DELETE, HEAD.', + }, + null, + 2, + ), + { + status: 405, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) +} + +function validateUrl(url) { + // quick and dirty validation + if (url == '') { + return false + } + try { + new URL(url) + } catch (TypeError) { + return false + } + return true +} + +async function putLink(givenSecret, shorturl, key, url) { + if (givenSecret != secret) { + return new Response( + JSON.stringify( + { + code: '401 Unauthorized', + message: 'Unauthorized.', + }, + null, + 2, + ), + { + status: 401, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + + if (url == null || !validateUrl(url)) { + return new Response( + JSON.stringify( + { + code: '400 Bad Request', + message: "No valid URL given. Please set a 'URL' header.", + }, + null, + 2, + ), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + } + + await kv.put(key, url) + return new Response( + JSON.stringify( + { + message: 'URL created succesfully.', + key: key.substr(1), + shorturl: shorturl, + longurl: url, + }, + null, + 2, + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1562540 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "private": true, + "name": "worker-links", + "version": "1.0.0", + "description": "Simple link shortener for Cloudflare Workers.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "format": "prettier --write '**/*.{js,css,json,md}'" + }, + "author": "Erisa A", + "license": "MIT", + "devDependencies": { + "prettier": "^1.18.2" + } +} diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..5ee3357 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,11 @@ +name = "worker-links" +type = "javascript" + +# Change these!! +account_id = "ece1d09b06af2ced51407c97505ea0cc" +zone_id = "e2eabc0f0319355c842fd443ad480ade" +kv_namespaces = [ { binding = "kv", id = "1be44406edc142a084435e24dbf8ae1d", preview_id = "15cfb90ecd654b8f8a9ccd600832093f" }] + +# Remove or comment out the route line if using workers_dev +workers_dev = false +route = "erisa.link/*"