@@ -244,3 +244,138 @@ fn test_http_proxy_do() {
244244 println ('Proxy env vars (HTTP_PROXY or HTTPS_PROXY) not set. Skipping test.' )
245245 }
246246}
247+
248+ const multipart_https_payload_len = 20 * 1024 + 137
249+
250+ fn test_https_multipart_form_preserves_large_binary_body () ! {
251+ mut port_listener := net.listen_tcp (.ip, '127.0.0.1:0' )!
252+ port := port_listener.addr ()! .port ()!
253+ port_listener.close ()!
254+
255+ payload := multipart_https_test_payload ()
256+ form := {
257+ 'alpha' : 'beta'
258+ }
259+ files := {
260+ 'file' : [
261+ FileData{
262+ filename: 'payload.bin'
263+ content_type: 'application/octet-stream'
264+ data: payload
265+ },
266+ ]
267+ }
268+ body , boundary := multipart_form_body (form, files)
269+
270+ mut listener := mbedtls.new_ssl_listener ('127.0.0.1:${port }' , mbedtls.SSLConnectConfig{
271+ cert: proxy_https_test_cert_path
272+ cert_key: proxy_https_test_key_path
273+ validate: false
274+ })!
275+ server := spawn multipart_https_serve_once (mut listener, body, boundary, form, files)
276+
277+ mut header := new_header ()
278+ header.set (.content_type, 'multipart/form-data; boundary="${boundary }"' )
279+ resp := fetch (
280+ method: .post
281+ url: 'https://127.0.0.1:${port }/upload'
282+ header: header
283+ data: body
284+ validate: false
285+ )!
286+ server.wait ()
287+
288+ assert resp.status_code == 200
289+ assert resp.body == 'ok'
290+ }
291+
292+ fn multipart_https_test_payload () string {
293+ mut payload := []u8 {len: multipart_https_payload_len, init: u8 (((index * 17 ) % 250 ) + 1 )}
294+ payload[127 ] = 0
295+ payload[4096 ] = 0
296+ payload[16 * 1024 ] = 0
297+ payload[payload.len - 1 ] = `!`
298+ return payload.bytestr ()
299+ }
300+
301+ fn multipart_https_serve_once (mut listener mbedtls.SSLListener, expected_body string , boundary string , expected_form map [string ]string , expected_files map [string ][]FileData) {
302+ defer {
303+ listener.shutdown () or {}
304+ }
305+ mut conn := listener.accept () or { panic (err) }
306+ conn.set_read_timeout (5 * time.second)
307+ defer {
308+ conn.shutdown () or {}
309+ }
310+
311+ request_text := read_https_request (mut conn) or { panic (err) }
312+ req := parse_request_str (request_text) or { panic (err) }
313+
314+ assert req.method == .post
315+ assert req.url == '/upload'
316+ assert req.data == expected_body
317+ assert req.data.len == expected_body.len
318+ assert req.header.get (.content_length) or { panic (err) } == expected_body.len.str ()
319+ assert req.header.get (.content_type) or { panic (err) } == 'multipart/form-data; boundary="${boundary }"'
320+
321+ form , files := parse_multipart_form (req.data, boundary)
322+ assert form == expected_form
323+ assert files == expected_files
324+
325+ conn.write_string ('HTTP/1.1 200 OK\r\n Content-Length: 2\r\n Connection: close\r\n\r\n ok' ) or {
326+ panic (err)
327+ }
328+ }
329+
330+ fn read_https_request (mut conn mbedtls.SSLConn) ! string {
331+ mut request := []u8 {}
332+ mut buf := []u8 {len: 1024 }
333+ mut content_length := - 1
334+ mut headers_end := - 1
335+
336+ for {
337+ n := conn.read (mut buf) or {
338+ if err.code () == net.err_timed_out_code {
339+ return error ('timed out while reading HTTPS request' )
340+ }
341+ return err
342+ }
343+ if n < = 0 {
344+ break
345+ }
346+ request << buf[..n]
347+
348+ request_str := request.bytestr ()
349+ if headers_end == - 1 {
350+ headers_end = request_str.index ('\r\n\r\n ' ) or { - 1 }
351+ if headers_end != - 1 {
352+ headers := request_str[..headers_end]
353+ for line in headers.split ('\r\n ' ) {
354+ if line.to_lower ().starts_with ('content-length:' ) {
355+ content_length = line.all_after (':' ).trim_space ().int ()
356+ break
357+ }
358+ }
359+ }
360+ }
361+
362+ if headers_end != - 1 && content_length > = 0 {
363+ body_start := headers_end + 4
364+ if request.len - body_start > = content_length {
365+ break
366+ }
367+ }
368+ }
369+
370+ if headers_end == - 1 {
371+ return error ('HTTPS request did not include a full header block' )
372+ }
373+ if content_length < 0 {
374+ return error ('HTTPS request did not include Content-Length' )
375+ }
376+ body_start := headers_end + 4
377+ if request.len - body_start < content_length {
378+ return error ('HTTPS request body was truncated: expected ${content_length } bytes, got ${request .len - body_start }' )
379+ }
380+ return request.bytestr ()
381+ }
0 commit comments