commit
5556cea72f
3 changed files with 114 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ worker/
|
||||||
node_modules/
|
node_modules/
|
||||||
.cargo-ok
|
.cargo-ok
|
||||||
wrangler.prod.toml
|
wrangler.prod.toml
|
||||||
|
.mf
|
||||||
29
README.md
29
README.md
|
|
@ -55,7 +55,7 @@ server: cloudflare
|
||||||
..other ephemeral headers..
|
..other ephemeral headers..
|
||||||
```
|
```
|
||||||
|
|
||||||
To create or update a custom short URl, send a `PUT` to the intended target URL:
|
To create or update a custom short URL, send a `PUT` to the intended target URL:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
erisa@Tuturu:~$ curl -X PUT -H "Authorization: mysecret" -H "URL: https://erisa.uk" https://erisa.link/mywebsite
|
erisa@Tuturu:~$ curl -X PUT -H "Authorization: mysecret" -H "URL: https://erisa.uk" https://erisa.link/mywebsite
|
||||||
|
|
@ -79,15 +79,38 @@ erisa@Tuturu:~$ curl -X DELETE -H "Authorization: mysecret" https://erisa.link/k
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also bulk create multiple shortlinks at once by sending a `POST` to `/` with no `URL` header and with a JSON body instead:
|
||||||
|
|
||||||
|
```json
|
||||||
|
erisa@Tuturu:~$ curl -X POST -H "Authorization: mysecret" https://erisa.link/ \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{ "/short1": "https://example.com", "/mywebsite": "https://erisa.uk" }'
|
||||||
|
{
|
||||||
|
"message": "URLs created successfully",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"key": "short1",
|
||||||
|
"shorturl": "https://erisa.link/short1",
|
||||||
|
"longurl": "https://example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "mywebsite",
|
||||||
|
"shorturl": "http://erisa.link/mywebsite",
|
||||||
|
"longurl": "https://erisa.uk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
It is a planned feature to be able to list all URLs via a `GET` on `/` with `Authorization`.
|
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:
|
For the time being you can view them from your Cloudflare Dashboard:
|
||||||
Cloudflare Dashboard -> Workers -> KV -> View on the namespace.
|
Cloudflare Dashboard -> Workers -> KV -> View on the namespace.
|
||||||
|
|
||||||
## Plausible Analytics
|
## Plausible Analytics
|
||||||
|
|
||||||
To get statistics for your short URLs with Plausible Analytics, define a `PLAUSIBLE_HOST` secret set to the URL of your Plausible instance. For example, `https://plausible.io/`.
|
To get statistics for your short URLs with Plausible Analytics, define a `PLAUSIBLE_HOST` secret set to the URL of your Plausible instance. For example, `https://plausible.io/`.
|
||||||
|
|
||||||
## Security
|
## 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: `erisa (at) erisa.uk` with any relevant details.
|
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: `erisa (at) erisa.uk` with any relevant details.
|
||||||
|
|
|
||||||
100
index.js
100
index.js
|
|
@ -19,8 +19,8 @@ async function handleRequest(event) {
|
||||||
|
|
||||||
let path = new URL(request.url).pathname
|
let path = new URL(request.url).pathname
|
||||||
// Trim trailing slash
|
// Trim trailing slash
|
||||||
let key = path !== '/' ? path.replace(/\/$/, '') : path;
|
let key = path !== '/' ? path.replace(/\/$/, '') : path
|
||||||
let shorturl = new URL(request.url).origin + key
|
let shorturl = new URL(key, request.url).origin
|
||||||
|
|
||||||
if (request.method == 'PUT') {
|
if (request.method == 'PUT') {
|
||||||
return await putLink(
|
return await putLink(
|
||||||
|
|
@ -41,14 +41,77 @@ async function handleRequest(event) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
key = '/' + Math.random().toString(36).slice(5)
|
const body = await request.text()
|
||||||
shorturl = new URL(request.url).origin + key
|
|
||||||
return await putLink(
|
if (!body) {
|
||||||
request.headers.get('Authorization'),
|
key = '/' + Math.random().toString(36).slice(5)
|
||||||
shorturl,
|
shorturl = new URL(key, request.url)
|
||||||
key,
|
return await putLink(
|
||||||
request.headers.get('URL'),
|
request.headers.get('Authorization'),
|
||||||
)
|
shorturl,
|
||||||
|
key,
|
||||||
|
request.headers.get('URL'),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (request.headers.get('Authorization') != secret) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
code: '401 Unauthorized',
|
||||||
|
message: 'Unauthorized.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let json
|
||||||
|
try {
|
||||||
|
json = JSON.parse(body)
|
||||||
|
} catch {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
code: '400 Bad Request',
|
||||||
|
message: 'Body must be valid JSON, or none at all.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = validateBulkBody(json)
|
||||||
|
if (!valid) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
code: '400 Bad Request',
|
||||||
|
message:
|
||||||
|
'Body must be a standard JSON object mapping keys to urls.',
|
||||||
|
example: {
|
||||||
|
'/short': 'https://example.com/really-really-really-long-1',
|
||||||
|
'/other': 'https://subdomain.example.com/and-some-long-path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, url] of Object.entries(json)) {
|
||||||
|
await kv.put(key, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
message: 'URLs created successfully',
|
||||||
|
entries: Object.entries(json).map(([key, longurl]) => ({
|
||||||
|
key: key.slice(1),
|
||||||
|
shorturl: new URL(key, request.url),
|
||||||
|
longurl,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (request.method == 'GET' || request.method == 'HEAD') {
|
} else if (request.method == 'GET' || request.method == 'HEAD') {
|
||||||
let url = await kv.get(key)
|
let url = await kv.get(key)
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
|
|
@ -106,7 +169,7 @@ async function handleRequest(event) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
shorturl = new URL(request.url).origin + key
|
shorturl = new URL(key, request.url)
|
||||||
let url = await kv.get(key)
|
let url = await kv.get(key)
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -123,7 +186,7 @@ async function handleRequest(event) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
message: 'Short URL deleted succesfully.',
|
message: 'Short URL deleted succesfully.',
|
||||||
key: key.substr(1),
|
key: key.slice(1),
|
||||||
shorturl: shorturl,
|
shorturl: shorturl,
|
||||||
longurl: url,
|
longurl: url,
|
||||||
},
|
},
|
||||||
|
|
@ -159,6 +222,17 @@ function validateUrl(url) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zod and other validation libs too
|
||||||
|
function validateBulkBody(body) {
|
||||||
|
// Starting `/` and no ending `/`
|
||||||
|
const keyRe = /^\/.*?[^\/]$/
|
||||||
|
|
||||||
|
if (!body || typeof body !== 'object' || Array.isArray(body)) return false
|
||||||
|
return Object.entries(body).every(
|
||||||
|
([key, url]) => keyRe.test(key) && validateUrl(url),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function putLink(givenSecret, shorturl, key, url) {
|
async function putLink(givenSecret, shorturl, key, url) {
|
||||||
if (givenSecret != secret) {
|
if (givenSecret != secret) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -188,7 +262,7 @@ async function putLink(givenSecret, shorturl, key, url) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
message: 'URL created succesfully.',
|
message: 'URL created succesfully.',
|
||||||
key: key.substr(1),
|
key: key.slice(1),
|
||||||
shorturl: shorturl,
|
shorturl: shorturl,
|
||||||
longurl: url,
|
longurl: url,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue