Reinitalize repository
This commit is contained in:
commit
44aac0fe67
8 changed files with 369 additions and 0 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
github: Erisa
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/target
|
||||||
|
/dist
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
bin/
|
||||||
|
pkg/
|
||||||
|
wasm-pack.log
|
||||||
|
worker/
|
||||||
|
node_modules/
|
||||||
|
.cargo-ok
|
||||||
|
wrangler.prod.toml
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
25
LICENSE
Normal file
25
LICENSE
Normal file
|
|
@ -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.
|
||||||
78
README.md
Normal file
78
README.md
Normal file
|
|
@ -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.
|
||||||
220
index.js
Normal file
220
index.js
Normal file
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wrangler.toml
Normal file
11
wrangler.toml
Normal file
|
|
@ -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/*"
|
||||||
Loading…
Add table
Reference in a new issue