|
| 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