v/vlib/vweb/request.v

158 lines
3.9 KiB
V

module vweb
import io
import strings
import net.http
import net.urllib
fn parse_request(mut reader io.BufferedReader) ?http.Request {
// request line
mut line := reader.read_line() ?
method, target, version := parse_request_line(line) ?
// headers
mut header := http.new_header()
line = reader.read_line() ?
for line != '' {
key, value := parse_header(line) ?
header.add_custom(key, value) ?
line = reader.read_line() ?
}
header.coerce(canonicalize: true)
// body
mut body := []byte{}
if length := header.get(.content_length) {
n := length.int()
if n > 0 {
body = []byte{len: n}
mut count := 0
for count < body.len {
count += reader.read(mut body[count..]) or { break }
}
}
}
return http.Request{
method: method
url: target.str()
header: header
data: body.bytestr()
version: version
}
}
fn parse_request_line(s string) ?(http.Method, urllib.URL, http.Version) {
words := s.split(' ')
if words.len != 3 {
return error('malformed request line')
}
method := http.method_from_str(words[0])
target := urllib.parse(words[1]) ?
version := http.version_from_str(words[2])
if version == .unknown {
return error('unsupported version')
}
return method, target, version
}
fn parse_header(s string) ?(string, string) {
if !s.contains(':') {
return error('missing colon in header')
}
words := s.split_nth(':', 2)
// TODO: parse quoted text according to the RFC
return words[0], words[1].trim_left(' \t')
}
// Parse URL encoded key=value&key=value forms
fn parse_form(body string) map[string]string {
words := body.split('&')
mut form := map[string]string{}
for word in words {
kv := word.split_nth('=', 2)
if kv.len != 2 {
continue
}
key := urllib.query_unescape(kv[0]) or { continue }
val := urllib.query_unescape(kv[1]) or { continue }
form[key] = val
}
return form
// }
// todo: parse form-data and application/json
// ...
}
fn parse_multipart_form(body string, boundary string) (map[string]string, map[string][]FileData) {
sections := body.split(boundary)
fields := sections[1..sections.len - 1]
mut form := map[string]string{}
mut files := map[string][]FileData{}
for field in fields {
// TODO: do not split into lines; do same parsing for HTTP body
lines := field.split('\n')[1..]
disposition := parse_disposition(lines[0])
// Grab everything between the double quotes
name := disposition['name'] or { continue }
// Parse files
// TODO: filename*
if 'filename' in disposition {
filename := disposition['filename']
// Parse Content-Type header
if lines.len == 1 || !lines[1].to_lower().starts_with('content-type:') {
continue
}
mut ct := lines[1].split_nth(':', 2)[1]
ct = ct.trim_left(' \t').trim_right('\r')
data := lines_to_string(field.len, lines, 3, lines.len - 1)
files[name] << FileData{
filename: filename
content_type: ct
data: data
}
continue
}
data := lines_to_string(field.len, lines, 2, lines.len - 1)
form[name] = data
}
return form, files
}
// Parse the Content-Disposition header of a multipart form
// Returns a map of the key="value" pairs
// Example: parse_disposition('Content-Disposition: form-data; name="a"; filename="b"') == {'name': 'a', 'filename': 'b'}
fn parse_disposition(line string) map[string]string {
mut data := map[string]string{}
for word in line.split(';') {
kv := word.split_nth('=', 2)
if kv.len != 2 {
continue
}
key, value := kv[0].to_lower().trim_left(' \t'), kv[1].trim_right('\r')
if value.starts_with('"') && value.ends_with('"') {
data[key] = value[1..value.len - 1]
} else {
data[key] = value
}
}
return data
}
[manualfree]
fn lines_to_string(len int, lines []string, start int, end int) string {
mut sb := strings.new_builder(len)
for i in start .. end {
sb.writeln(lines[i])
}
sb.cut_last(1) // last newline
if sb.last_n(1) == '\r' {
sb.cut_last(1)
}
res := sb.str()
unsafe { sb.free() }
return res
}