11import { verifyAuth } from "../auth.js"
2- import { FormDataPart , getBoundary , parseFormdata } from "../parseFormdata.js"
32import { decode , genRandStr , isLegalUrl , WorkerError } from "../common.js"
43import { createPaste , getPasteMetadata , pasteNameAvailable , updatePaste } from "../storage/storage.js"
54import {
@@ -12,18 +11,55 @@ import {
1211 PASSWD_SEP ,
1312 parseExpiration ,
1413 parsePath ,
14+ MIN_PASSWD_LEN ,
15+ MAX_PASSWD_LEN ,
1516} from "../shared.js"
1617
17- function suggestUrl ( content : ArrayBuffer , short : string , baseUrl : string , filename ?: string ) {
18+ function suggestUrl ( short : string , baseUrl : string , filename ?: string , contentAsString ?: string ) {
19+ // TODO: should we suggest for URL redirect?
1820 if ( filename ) {
1921 return `${ baseUrl } /${ short } /${ filename } `
20- } else if ( isLegalUrl ( decode ( content ) ) ) {
22+ } else if ( contentAsString && isLegalUrl ( contentAsString ) ) {
2123 return `${ baseUrl } /u/${ short } `
2224 } else {
2325 return undefined
2426 }
2527}
2628
29+ async function getStringFromPart ( part : string | null | File ) : Promise < string | undefined > {
30+ if ( part === null || typeof part == "string" ) {
31+ return part || undefined
32+ } else {
33+ return decode ( await part . arrayBuffer ( ) )
34+ }
35+ }
36+
37+ async function getFileFromPart ( part : string | null | File ) : Promise < {
38+ filename ?: string
39+ content ?: ReadableStream | ArrayBuffer
40+ contentAsString ?: string
41+ contentLength : number
42+ } > {
43+ if ( part === null ) {
44+ return { contentLength : 0 }
45+ } else if ( typeof part == "string" ) {
46+ const encoded = new TextEncoder ( ) . encode ( part )
47+ return { filename : undefined , content : encoded . buffer , contentAsString : part , contentLength : encoded . length }
48+ } else {
49+ if ( part . size < 1000 ) {
50+ const arrayBuffer = await part . arrayBuffer ( )
51+ return {
52+ filename : part . name ,
53+ content : arrayBuffer ,
54+ contentAsString : decode ( arrayBuffer ) ,
55+ contentLength : part . size ,
56+ }
57+ } else {
58+ return { filename : part . name , content : part . stream ( ) , contentLength : part . size }
59+ }
60+ }
61+ }
62+
2763export async function handlePostOrPut (
2864 request : Request ,
2965 env : Env ,
@@ -42,32 +78,22 @@ export async function handlePostOrPut(
4278 const url = new URL ( request . url )
4379
4480 // parse formdata
45- let form : Map < string , FormDataPart > = new Map ( )
46- if ( contentType . includes ( "multipart/form-data" ) ) {
47- // because cloudflare runtime treat all formdata part as strings thus corrupting binary data,
48- // we need to manually parse formdata
49- const uint8Array = new Uint8Array ( await request . arrayBuffer ( ) )
50- try {
51- form = parseFormdata ( uint8Array , getBoundary ( contentType ) )
52- } catch {
53- throw new WorkerError ( 400 , "error occurs when parsing formdata" )
54- }
55- } else {
81+ if ( ! contentType . includes ( "multipart/form-data" ) ) {
5682 throw new WorkerError ( 400 , `bad usage, please use 'multipart/form-data' instead of ${ contentType } ` )
5783 }
5884
59- const content = form . get ( "c" ) ?. content
60- const filename = form . get ( "c" ) && form . get ( "c" ) ! . disposition . filename
61- const nameFromForm = form . get ( "n" ) && decode ( form . get ( "n" ) ! . content )
62- const isPrivate = form . get ( "p" )
63- const passwdFromForm = form . get ( "s" ) && decode ( form . get ( "s" ) ! . content )
64- const expire : string =
65- form . has ( "e" ) && form . get ( "e" ) ! . content . byteLength > 0 ? decode ( form . get ( "e" ) ! . content ) : env . DEFAULT_EXPIRATION
85+ const form = await request . formData ( )
86+ const { filename, content , contentAsString , contentLength } = await getFileFromPart ( form . get ( "c" ) )
87+ const nameFromForm = await getStringFromPart ( form . get ( "n" ) )
88+ const isPrivate = form . get ( "p" ) !== null
89+ const passwdFromForm = await getStringFromPart ( form . get ( "s" ) )
90+ const expireFromPart : string | undefined = await getStringFromPart ( form . get ( "e" ) )
91+ const expire = expireFromPart !== undefined ? expireFromPart : env . DEFAULT_EXPIRATION
6692
6793 // check if paste content is legal
6894 if ( content === undefined ) {
6995 throw new WorkerError ( 400 , "cannot find content in formdata" )
70- } else if ( content . length > MAX_LEN ) {
96+ } else if ( contentLength > MAX_LEN ) {
7197 throw new WorkerError ( 413 , "payload too large" )
7298 }
7399
@@ -81,6 +107,18 @@ export async function handlePostOrPut(
81107 expirationSeconds = maxExpiration
82108 }
83109
110+ // check if password is legal
111+ // TODO: sync checks to frontend
112+ if ( passwdFromForm ) {
113+ if ( passwdFromForm . length > MAX_PASSWD_LEN ) {
114+ throw new WorkerError ( 400 , `password too long (${ passwdFromForm . length } > ${ MAX_PASSWD_LEN } )` )
115+ } else if ( passwdFromForm . length < MIN_PASSWD_LEN ) {
116+ throw new WorkerError ( 400 , `password too short (${ passwdFromForm . length } < ${ MIN_PASSWD_LEN } )` )
117+ } else if ( passwdFromForm . includes ( "\n" ) ) {
118+ throw new WorkerError ( 400 , `password should not contain newline` )
119+ }
120+ }
121+
84122 // check if name is legal
85123 if ( nameFromForm !== undefined && ! NAME_REGEX . test ( nameFromForm ) ) {
86124 throw new WorkerError ( 400 , `Name ${ nameFromForm } not satisfying regexp ${ NAME_REGEX } ` )
@@ -118,11 +156,12 @@ export async function handlePostOrPut(
118156 expirationSeconds,
119157 now,
120158 passwd : newPasswd ,
159+ contentLength,
121160 filename,
122161 } )
123162 return makeResponse ( {
124163 url : accessUrl ( pasteName ) ,
125- suggestedUrl : suggestUrl ( content , pasteName , env . DEPLOY_URL , filename ) ,
164+ suggestedUrl : suggestUrl ( pasteName , env . DEPLOY_URL , filename , contentAsString ) ,
126165 manageUrl : manageUrl ( pasteName , newPasswd ) ,
127166 expirationSeconds,
128167 expireAt : new Date ( now . getTime ( ) + 1000 * expirationSeconds ) . toISOString ( ) ,
@@ -148,11 +187,12 @@ export async function handlePostOrPut(
148187 now,
149188 passwd,
150189 filename,
190+ contentLength,
151191 } )
152192
153193 return makeResponse ( {
154194 url : accessUrl ( pasteName ) ,
155- suggestedUrl : suggestUrl ( content , pasteName , env . DEPLOY_URL , filename ) ,
195+ suggestedUrl : suggestUrl ( pasteName , env . DEPLOY_URL , filename , contentAsString ) ,
156196 manageUrl : manageUrl ( pasteName , passwd ) ,
157197 expirationSeconds,
158198 expireAt : new Date ( now . getTime ( ) + 1000 * expirationSeconds ) . toISOString ( ) ,
0 commit comments