158 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			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
 | 
						|
}
 |