Skip to content

Commit 660ae4d

Browse files
committed
refac: split source files
1 parent d75ec2a commit 660ae4d

File tree

11 files changed

+446
-442
lines changed

11 files changed

+446
-442
lines changed
File renamed without changes.

src/handlers/handleDelete.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { parsePath, WorkerError } from "../common.js"
2+
3+
export async function handleDelete(request, env, ctx) {
4+
const url = new URL(request.url)
5+
const { short, passwd } = parsePath(url.pathname)
6+
const item = await env.PB.getWithMetadata(short)
7+
if (item.value === null) {
8+
throw new WorkerError(404, `paste of name '${short}' not found`)
9+
} else {
10+
if (passwd !== item.metadata.passwd) {
11+
throw new WorkerError(403, `incorrect password for paste '${short}`)
12+
} else {
13+
await env.PB.delete(short)
14+
return new Response("the paste will be deleted in seconds")
15+
}
16+
}
17+
}

src/handlers/handleRead.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { decode, encodeRFC5987ValueChars, isLegalUrl, parsePath, WorkerError } from "../common.js"
2+
import { getStaticPage } from "../pages/staticPages.js"
3+
import { verifyAuth } from "../auth.js"
4+
import { getType } from "mime/lite.js"
5+
import { makeMarkdown } from "../pages/markdown.js"
6+
import { makeHighlight } from "../pages/highlight.js"
7+
8+
function staticPageCacheHeader(env) {
9+
const age = env.CACHE_STATIC_PAGE_AGE
10+
return age ? { "cache-control": `public, max-age=${age}` } : {}
11+
}
12+
13+
function pasteCacheHeader(env) {
14+
const age = env.CACHE_PASTE_AGE
15+
return age ? { "cache-control": `public, max-age=${age}` } : {}
16+
}
17+
18+
function lastModifiedHeader(paste) {
19+
const lastModified = paste.metadata.lastModified
20+
return lastModified ? { "last-modified": new Date(lastModified).toGMTString() } : {}
21+
}
22+
23+
export async function handleGet(request, env, ctx) {
24+
const url = new URL(request.url)
25+
const { role, short, ext, passwd, filename } = parsePath(url.pathname)
26+
27+
if (url.pathname === "/favicon.ico" && env.FAVICON) {
28+
return Response.redirect(env.FAVICON)
29+
}
30+
31+
// return the editor for admin URL
32+
const staticPageContent = getStaticPage((passwd.length > 0) ? "/" : url.pathname, env)
33+
if (staticPageContent) {
34+
// access to all static pages requires auth
35+
const authResponse = verifyAuth(request, env)
36+
if (authResponse !== null) {
37+
return authResponse
38+
}
39+
return new Response(staticPageContent, {
40+
headers: { "content-type": "text/html;charset=UTF-8", ...staticPageCacheHeader(env) },
41+
})
42+
}
43+
44+
const mime = url.searchParams.get("mime") || getType(ext) || "text/plain"
45+
46+
const disp = url.searchParams.has("a") ? "attachment" : "inline"
47+
48+
const item = await env.PB.getWithMetadata(short, { type: "arrayBuffer" })
49+
50+
// when paste is not found
51+
if (item.value === null) {
52+
throw new WorkerError(404, `paste of name '${short}' not found`)
53+
}
54+
55+
// check `if-modified-since`
56+
const pasteLastModified = item.metadata.lastModified
57+
const headerModifiedSince = request.headers.get("if-modified-since")
58+
if (pasteLastModified && headerModifiedSince) {
59+
let pasteLastModifiedMs = Date.parse(pasteLastModified)
60+
pasteLastModifiedMs -= pasteLastModifiedMs % 1000 // deduct the milliseconds parts
61+
const headerIfModifiedMs = Date.parse(headerModifiedSince)
62+
if (pasteLastModifiedMs <= headerIfModifiedMs) {
63+
return new Response(null, {
64+
status: 304, // Not Modified
65+
headers: lastModifiedHeader(item),
66+
})
67+
}
68+
}
69+
70+
// determine filename with priority: url path > meta
71+
const returnFilename = filename || item.metadata.filename
72+
73+
// handle URL redirection
74+
if (role === "u") {
75+
const redirectURL = decode(item.value)
76+
if (isLegalUrl(redirectURL)) {
77+
return Response.redirect(redirectURL)
78+
} else {
79+
throw new WorkerError(400, "cannot parse paste content as a legal URL")
80+
}
81+
}
82+
83+
// handle article (render as markdown)
84+
if (role === "a") {
85+
const md = makeMarkdown(decode(item.value))
86+
return new Response(md, {
87+
headers: { "content-type": `text/html;charset=UTF-8`, ...pasteCacheHeader(env), ...lastModifiedHeader(item) },
88+
})
89+
}
90+
91+
// handle language highlight
92+
const lang = url.searchParams.get("lang")
93+
if (lang) {
94+
return new Response(makeHighlight(decode(item.value), lang), {
95+
headers: { "content-type": `text/html;charset=UTF-8`, ...pasteCacheHeader(env), ...lastModifiedHeader(item) },
96+
})
97+
} else {
98+
99+
// handle default
100+
const headers = { "content-type": `${mime};charset=UTF-8`, ...pasteCacheHeader(env), ...lastModifiedHeader(item) }
101+
if (returnFilename) {
102+
const encodedFilename = encodeRFC5987ValueChars(returnFilename)
103+
headers["content-disposition"] = `${disp}; filename*=UTF-8''${encodedFilename}`
104+
} else {
105+
headers["content-disposition"] = `${disp}`
106+
}
107+
return new Response(item.value, { headers })
108+
}
109+
}

src/handlers/handleWrite.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { verifyAuth } from "../auth.js"
2+
import { getBoundary, parseFormdata } from "../parseFormdata.js"
3+
import { decode, genRandStr, getDispFilename, params, parseExpiration, parsePath, WorkerError } from "../common.js"
4+
5+
async function createPaste(env, content, isPrivate, expire, short, createDate, passwd, filename) {
6+
createDate = createDate || new Date().toISOString()
7+
passwd = passwd || genRandStr(params.ADMIN_PATH_LEN)
8+
const short_len = isPrivate ? params.PRIVATE_RAND_LEN : params.RAND_LEN
9+
10+
if (short === undefined) {
11+
while (true) {
12+
short = genRandStr(short_len)
13+
if ((await env.PB.get(short)) === null) break
14+
}
15+
}
16+
17+
await env.PB.put(short, content, {
18+
expirationTtl: expire,
19+
metadata: {
20+
postedAt: createDate,
21+
passwd: passwd,
22+
filename: filename,
23+
lastModified: new Date().toISOString(),
24+
},
25+
})
26+
let accessUrl = env.BASE_URL + "/" + short
27+
const adminUrl = env.BASE_URL + "/" + short + params.SEP + passwd
28+
return {
29+
url: accessUrl,
30+
suggestUrl: suggestUrl(content, filename, short, env.BASE_URL),
31+
admin: adminUrl,
32+
isPrivate: isPrivate,
33+
expire: expire || null,
34+
}
35+
}
36+
37+
function suggestUrl(content, filename, short, baseUrl) {
38+
function isUrl(text) {
39+
try {
40+
new URL(text)
41+
return true
42+
} catch (e) {
43+
return false
44+
}
45+
}
46+
47+
if (filename) {
48+
return `${baseUrl}/${short}/${filename}`
49+
} else if (isUrl(decode(content))) {
50+
return `${baseUrl}/u/${short}`
51+
} else {
52+
return null
53+
}
54+
}
55+
56+
export async function handlePostOrPut(request, env, ctx, isPut) {
57+
if (!isPut) { // only POST requires auth, since PUT request already contains auth
58+
const authResponse = verifyAuth(request, env)
59+
if (authResponse !== null) {
60+
return authResponse
61+
}
62+
}
63+
64+
const contentType = request.headers.get("content-type") || ""
65+
const url = new URL(request.url)
66+
67+
// parse formdata
68+
let form = {}
69+
if (contentType.includes("multipart/form-data")) {
70+
// because cloudflare runtime treat all formdata part as strings thus corrupting binary data,
71+
// we need to manually parse formdata
72+
const uint8Array = new Uint8Array(await request.arrayBuffer())
73+
try {
74+
form = parseFormdata(uint8Array, getBoundary(contentType))
75+
} catch (e) {
76+
throw new WorkerError(400, "error occurs when parsing formdata")
77+
}
78+
} else {
79+
throw new WorkerError(400, `bad usage, please use 'multipart/form-data' instead of ${contentType}`)
80+
}
81+
const content = form.get("c") && form.get("c").content
82+
const filename = form.get("c") && getDispFilename(form.get("c").fields)
83+
const name = form.get("n") && decode(form.get("n").content)
84+
const isPrivate = form.get("p") !== undefined
85+
const newPasswd = form.get("s") && decode(form.get("s").content)
86+
const expire =
87+
form.has("e") && form.get("e").content.byteLength > 0
88+
? decode(form.get("e").content)
89+
: undefined
90+
91+
// check if paste content is legal
92+
if (content === undefined) {
93+
throw new WorkerError(400, "cannot find content in formdata")
94+
} else if (content.length > params.MAX_LEN) {
95+
throw new WorkerError(413, "payload too large")
96+
}
97+
98+
// check if expiration is legal
99+
let expirationSeconds = undefined
100+
if (expire !== undefined) {
101+
expirationSeconds = parseExpiration(expire)
102+
if (isNaN(expirationSeconds)) {
103+
throw new WorkerError(400, `cannot parse expire ${expirationSeconds} as an number`)
104+
}
105+
if (expirationSeconds < 60) {
106+
throw new WorkerError(
107+
400,
108+
`due to limitation of Cloudflare, expire should be a integer greater than 60, '${expirationSeconds}' given`,
109+
)
110+
}
111+
}
112+
113+
// check if name is legal
114+
if (name !== undefined && !params.NAME_REGEX.test(name)) {
115+
throw new WorkerError(
116+
400,
117+
`Name ${name} not satisfying regexp ${params.NAME_REGEX}`,
118+
)
119+
}
120+
121+
function makeResponse(created) {
122+
return new Response(JSON.stringify(created, null, 2), {
123+
headers: { "content-type": "application/json;charset=UTF-8" },
124+
})
125+
}
126+
127+
if (isPut) {
128+
const { short, passwd } = parsePath(url.pathname)
129+
const item = await env.PB.getWithMetadata(short)
130+
if (item.value === null) {
131+
throw new WorkerError(404, `paste of name '${short}' is not found`)
132+
} else {
133+
const date = item.metadata.postedAt
134+
if (passwd !== item.metadata.passwd) {
135+
throw new WorkerError(403, `incorrect password for paste '${short}`)
136+
} else {
137+
return makeResponse(
138+
await createPaste(env, content, isPrivate, expirationSeconds, short, date, newPasswd || passwd, filename),
139+
)
140+
}
141+
}
142+
} else {
143+
let short = undefined
144+
if (name !== undefined) {
145+
short = "~" + name
146+
if ((await env.PB.get(short)) !== null)
147+
throw new WorkerError(409, `name '${name}' is already used`)
148+
}
149+
return makeResponse(await createPaste(
150+
env, content, isPrivate, expirationSeconds, short, undefined, newPasswd, filename,
151+
))
152+
}
153+
}

0 commit comments

Comments
 (0)