diff --git a/vlib/net/http/request.v b/vlib/net/http/request.v index 6502c51cb4..35b03a5de8 100644 --- a/vlib/net/http/request.v +++ b/vlib/net/http/request.v @@ -304,14 +304,19 @@ pub fn (err MultiplePathAttributesError) msg() string { // HTTP request body. It is the inverse of parse_multipart_form. Returns // (body, boundary). // Note: Form keys should not contain quotes +[manualfree] fn multipart_form_body(form map[string]string, files map[string][]FileData) (string, string) { - alpha_numeric := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - boundary := rand.string_from_set(alpha_numeric, 64) - + rboundary := rand.ulid() + defer { + unsafe { rboundary.free() } + } mut sb := strings.new_builder(1024) + defer { + unsafe { sb.free() } + } for name, value in form { sb.write_string('\r\n--') - sb.write_string(boundary) + sb.write_string(rboundary) sb.write_string('\r\nContent-Disposition: form-data; name="') sb.write_string(name) sb.write_string('"\r\n\r\n') @@ -320,7 +325,7 @@ fn multipart_form_body(form map[string]string, files map[string][]FileData) (str for name, fs in files { for f in fs { sb.write_string('\r\n--') - sb.write_string(boundary) + sb.write_string(rboundary) sb.write_string('\r\nContent-Disposition: form-data; name="') sb.write_string(name) sb.write_string('"; filename="') @@ -332,9 +337,9 @@ fn multipart_form_body(form map[string]string, files map[string][]FileData) (str } } sb.write_string('\r\n--') - sb.write_string(boundary) + sb.write_string(rboundary) sb.write_string('--') - return sb.str(), boundary + return sb.str(), rboundary } struct LineSegmentIndexes { diff --git a/vlib/net/http/request_test.v b/vlib/net/http/request_test.v index d62828b807..3df2a570b4 100644 --- a/vlib/net/http/request_test.v +++ b/vlib/net/http/request_test.v @@ -154,6 +154,28 @@ ${contents[1]}\r } } +fn test_parse_multipart_form2() { + boundary := '---------------------------27472781931927549291906391339' + data := '--${boundary}\r +Content-Disposition: form-data; name="username"\r +\r +admin\r +--${boundary}\r +Content-Disposition: form-data; name="password"\r +\r +admin123\r +--${boundary}--\r +' + form, files := parse_multipart_form(data, boundary) + for k, v in form { + eprintln('> k: ${k} | v: ${v}') + eprintln('>> k.bytes(): ${k.bytes()}') + eprintln('>> v.bytes(): ${v.bytes()}') + } + assert form['username'] == 'admin' + assert form['password'] == 'admin123' +} + fn test_multipart_form_body() { files := { 'foo': [ @@ -175,7 +197,7 @@ fn test_multipart_form_body() { } fn test_parse_large_body() { - body := 'A'.repeat(101) // greater than max_bytes + body := 'A'.repeat(10_001) // greater than max_bytes req := 'GET / HTTP/1.1\r\nContent-Length: ${body.len}\r\n\r\n${body}' mut reader_ := reader(req) result := parse_request(mut reader_)! diff --git a/vlib/vweb/parse.v b/vlib/vweb/parse.v index 1b389f4883..9a82e01011 100644 --- a/vlib/vweb/parse.v +++ b/vlib/vweb/parse.v @@ -67,22 +67,25 @@ fn parse_query_from_url(url urllib.URL) map[string]string { return query } +const boundary_start = 'boundary=' + fn parse_form_from_request(request http.Request) !(map[string]string, map[string][]http.FileData) { - mut form := map[string]string{} - mut files := map[string][]http.FileData{} - if request.method in methods_with_form { - ct := request.header.get(.content_type) or { '' }.split(';').map(it.trim_left(' \t')) - if 'multipart/form-data' in ct { - boundary := ct.filter(it.starts_with('boundary=')) - if boundary.len != 1 { - return error('detected more that one form-data boundary') - } - // omit 'boundary="' and the last '"' - boundary_str := boundary[0].substr(10, boundary[0].len - 1) - form, files = http.parse_multipart_form(request.data, boundary_str) - } else { - form = http.parse_form(request.data) - } + if request.method !in [http.Method.post, .put, .patch] { + return map[string]string{}, map[string][]http.FileData{} } - return form, files + ct := request.header.get(.content_type) or { '' }.split(';').map(it.trim_left(' \t')) + if 'multipart/form-data' in ct { + boundaries := ct.filter(it.starts_with(vweb.boundary_start)) + if boundaries.len != 1 { + return error('detected more that one form-data boundary') + } + boundary := boundaries[0].all_after(vweb.boundary_start) + if boundary.len > 0 && boundary[0] == `"` { + // quotes are send by our http.post_multipart_form/2: + return http.parse_multipart_form(request.data, boundary.trim('"')) + } + // Firefox and other browsers, do not use quotes around the boundary: + return http.parse_multipart_form(request.data, boundary) + } + return http.parse_form(request.data), map[string][]http.FileData{} } diff --git a/vlib/vweb/tests/vweb_test.v b/vlib/vweb/tests/vweb_test.v index 2417e1b5c4..bd4fe0a549 100644 --- a/vlib/vweb/tests/vweb_test.v +++ b/vlib/vweb/tests/vweb_test.v @@ -238,6 +238,19 @@ fn test_http_client_multipart_form_data() { assert x.body == files[0].data } +fn test_login_with_multipart_form_data_send_by_fetch() { + mut form_config := http.PostMultipartFormConfig{ + form: { + 'username': 'myusername' + 'password': 'mypassword123' + } + } + x := http.post_multipart_form('http://${localserver}/login', form_config)! + assert x.status_code == 200 + assert x.status_msg == 'OK' + assert x.body == 'username: xmyusernamex | password: xmypassword123x' +} + fn test_host() { mut req := http.Request{ url: 'http://${localserver}/with_host' diff --git a/vlib/vweb/tests/vweb_test_server.v b/vlib/vweb/tests/vweb_test_server.v index 86be8022bb..1065c50a55 100644 --- a/vlib/vweb/tests/vweb_test_server.v +++ b/vlib/vweb/tests/vweb_test_server.v @@ -89,6 +89,11 @@ pub fn (mut app App) json_echo() vweb.Result { return app.ok(app.req.data) } +['/login'; post] +pub fn (mut app App) login_form(username string, password string) vweb.Result { + return app.html('username: x${username}x | password: x${password}x') +} + ['/form_echo'; post] pub fn (mut app App) form_echo() vweb.Result { app.set_content_type(app.req.header.get(.content_type) or { '' })