From 13769f440fb71061a2ce6681710bc47e1f519025 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Wed, 11 Dec 2019 16:32:54 +0200 Subject: [PATCH] vweb: continue after bad http client connection; performance fixes * Enable compiling vweb with -prod (by supressing 'declared and not used' warning about 'reset') . * Fix http responses (now wrk is happy and shows no errors) by adding a Content-Length header. * Fix -g compilation for urllib.v . * vweb: println action= only in debug mode. * vweb: max request headers counting fix. * Make vweb.html get a 'ctx mut Context' param, just like the other methods. * vweb: simplify add_header. * Use a string builder for the most common html case so that the response http text can be send in one go. * vweb: reduce _STR/string interpolation usage in the most common html response case. * vweb: refactor common http response formatting into Context.send_response_to_client/2 method. --- examples/vweb/vweb_example.v | 2 +- vlib/compiler/comptime.v | 2 +- vlib/compiler/fn.v | 2 +- vlib/net/socket.v | 20 ++++++-- vlib/net/urllib/urllib.v | 14 +++--- vlib/strings/builder_c.v | 4 +- vlib/strings/builder_js.v | 4 ++ vlib/vweb/vweb.v | 93 ++++++++++++++++++++---------------- 8 files changed, 85 insertions(+), 56 deletions(-) diff --git a/examples/vweb/vweb_example.v b/examples/vweb/vweb_example.v index c4197c3589..9d7b083d4f 100644 --- a/examples/vweb/vweb_example.v +++ b/examples/vweb/vweb_example.v @@ -22,7 +22,7 @@ pub fn (app mut App) init() { app.vweb.handle_static('.') } -pub fn (app & App) json_endpoint() { +pub fn (app mut App) json_endpoint() { app.vweb.json('{"a": 3}') } diff --git a/vlib/compiler/comptime.v b/vlib/compiler/comptime.v index abcc0e0ddc..33b01d3da8 100644 --- a/vlib/compiler/comptime.v +++ b/vlib/compiler/comptime.v @@ -178,7 +178,7 @@ fn (p mut Parser) comp_time() { p.genln('/////////////////// tmpl end') receiver := p.cur_fn.args[0] dot := if receiver.is_mut || receiver.ptr || receiver.typ.ends_with('*') { '->' } else { '.' } - p.genln('vweb__Context_html($receiver.name /*!*/$dot vweb, tmpl_res)') + p.genln('vweb__Context_html( & $receiver.name /*!*/$dot vweb, tmpl_res)') } else { p.error('bad comptime expr') diff --git a/vlib/compiler/fn.v b/vlib/compiler/fn.v index e09b5f7d83..e5d06f4a02 100644 --- a/vlib/compiler/fn.v +++ b/vlib/compiler/fn.v @@ -627,7 +627,7 @@ fn (p mut Parser) check_unused_and_mut_vars() { break } if !var.is_used && !p.pref.is_repl && !var.is_arg && - !p.pref.translated && var.name != 'tmpl_res' + !p.pref.translated && var.name != 'tmpl_res' && p.mod != 'vweb' { p.production_error_with_token_index('`$var.name` declared and not used', var.token_idx ) } diff --git a/vlib/net/socket.v b/vlib/net/socket.v index dc5fd205ab..44d541c7fe 100644 --- a/vlib/net/socket.v +++ b/vlib/net/socket.v @@ -202,13 +202,23 @@ pub fn dial(address string, port int) ?Socket { return s } -// send string data to socket +// send data to socket (when you have a memory buffer) pub fn (s Socket) send(buf byteptr, len int) ?int { - res := C.send(s.sockfd, buf, len, MSG_NOSIGNAL) - if res < 0 { - return error('net.send: failed with $res') + mut dptr := buf + mut dlen := len + for { + sbytes := C.send(s.sockfd, dptr, dlen, MSG_NOSIGNAL) + if sbytes < 0 { return error('net.send: failed with $sbytes') } + dlen -= sbytes + if dlen <= 0 { break } + dptr += sbytes } - return res + return len +} + +// send string data to socket (when you have a v string) +pub fn (s Socket) send_string(sdata string) ?int { + return s.send( sdata.str, sdata.len ) } // receive string data from socket diff --git a/vlib/net/urllib/urllib.v b/vlib/net/urllib/urllib.v index 199f75f48b..3745c7ab50 100644 --- a/vlib/net/urllib/urllib.v +++ b/vlib/net/urllib/urllib.v @@ -518,8 +518,8 @@ fn parse_url(rawurl string, via_request bool) ?URL { // RFC 3986, ยง3.3: // In addition, a URI reference (Section 4.1) may be a relative-path reference, // in which case the first path segment cannot contain a colon (':') character. - colon := rest.index(':') or { -1 } - slash := rest.index('/') or { -1 } + colon := rest.index(':') or { return error('there should be a : in the URL') } + slash := rest.index('/') or { return error('there should be a / in the URL') } if colon >= 0 && (slash < 0 || colon < slash) { // First path segment has colon. Not allowed in relative URL. return error(error_msg('parse_url: first path segment in URL cannot contain colon', '')) @@ -553,7 +553,7 @@ struct ParseAuthorityRes { fn parse_authority(authority string) ?ParseAuthorityRes { i := authority.last_index('@') mut host := '' - mut user := user('') + mut zuser := user('') if i < 0 { h := parse_host(authority) or { return error(err) @@ -566,7 +566,7 @@ fn parse_authority(authority string) ?ParseAuthorityRes { host = h } if i < 0 { - return ParseAuthorityRes{host: host, user: user} + return ParseAuthorityRes{host: host, user: zuser} } mut userinfo := authority[..i] if !valid_userinfo(userinfo) { @@ -577,7 +577,7 @@ fn parse_authority(authority string) ?ParseAuthorityRes { return error(err) } userinfo = u - user = user(userinfo) + zuser = user(userinfo) } else { mut username, mut password := split(userinfo, `:`, true) u := unescape(username, .encode_user_password) or { @@ -588,10 +588,10 @@ fn parse_authority(authority string) ?ParseAuthorityRes { return error(err) } password = p - user = user_password(username, password) + zuser = user_password(username, password) } return ParseAuthorityRes{ - user: user + user: zuser host: host } } diff --git a/vlib/strings/builder_c.v b/vlib/strings/builder_c.v index 2b8298d6c7..a9570144c9 100644 --- a/vlib/strings/builder_c.v +++ b/vlib/strings/builder_c.v @@ -9,11 +9,13 @@ mut: buf []byte pub: len int + initial_size int = 1 } pub fn new_builder(initial_size int) Builder { return Builder { buf: make(0, initial_size, 1) + initial_size: initial_size } } @@ -48,6 +50,6 @@ pub fn (b mut Builder) str() string { pub fn (b mut Builder) free() { unsafe{ free(b.buf.data) } - b.buf = make(0, 1, 1) + b.buf = make(0, b.initial_size, 1) b.len = 0 } diff --git a/vlib/strings/builder_js.v b/vlib/strings/builder_js.v index 1c2ac55d95..d5a4b7b947 100644 --- a/vlib/strings/builder_js.v +++ b/vlib/strings/builder_js.v @@ -9,11 +9,13 @@ mut: buf []byte pub: len int + initial_size int = 1 } pub fn new_builder(initial_size int) Builder { return Builder { buf: make(0, initial_size, sizeof(byte)) + initial_size: initial_size } } @@ -44,4 +46,6 @@ pub fn (b mut Builder) cut(n int) { } pub fn (b mut Builder) free() { + b.buf = make(0, b.initial_size, 1) + b.len = 0 } diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index d97802b8a3..4ad2c20aeb 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -9,13 +9,16 @@ import ( net http net.urllib + strings ) const ( methods_with_form = ['POST', 'PUT', 'PATCH'] - HEADER_SERVER = 'Server: VWeb\r\n' // TODO add to the headers - HTTP_404 = 'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\n404 Not Found' - HTTP_500 = 'HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\n500 Internal Server Error' + HEADER_SERVER = 'Server: VWeb\r\n' + HEADER_CONNECTION_CLOSE = 'Connection: close\r\n' + HEADERS_CLOSE = '${HEADER_SERVER}${HEADER_CONNECTION_CLOSE}\r\n' + HTTP_404 = 'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n${HEADERS_CLOSE}404 Not Found' + HTTP_500 = 'HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n${HEADERS_CLOSE}500 Internal Server Error' mime_types = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', @@ -44,37 +47,49 @@ mut: done bool } -pub fn (ctx Context) html(html string) { - if ctx.done { return } - //println('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n$ctx.headers\r\n\r\n$html') - ctx.conn.write('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n$ctx.headers\r\n\r\n$html') or { panic(err) } +fn (ctx mut Context) send_response_to_client(mimetype string, res string) bool { + if ctx.done { return false } + ctx.done = true + mut sb := strings.new_builder(1024) + sb.write('HTTP/1.1 200 OK\r\nContent-Type: ') sb.write(mimetype) + sb.write('\r\nContent-Length: ') sb.write(res.len.str()) + sb.write(ctx.headers) + sb.write('\r\n') + sb.write(HEADERS_CLOSE) + sb.write(res) + ctx.conn.send_string(sb.str()) or { return false } + sb.free() + return true +} + +pub fn (ctx mut Context) html(s string) { + ctx.send_response_to_client('text/html', s) } pub fn (ctx mut Context) text(s string) { + ctx.send_response_to_client('text/plain', s) +} + +pub fn (ctx mut Context) json(s string) { + ctx.send_response_to_client('application/json', s) +} + +pub fn (ctx mut Context) redirect(url string) { if ctx.done { return } - ctx.conn.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n' + - '$ctx.headers\r\n$s') or { panic(err) } ctx.done = true + ctx.conn.send_string('HTTP/1.1 302 Found\r\nLocation: ${url}${ctx.headers}\r\n${HEADERS_CLOSE}') or { return } } -pub fn (ctx Context) json(s string) { +pub fn (ctx mut Context) not_found(s string) { if ctx.done { return } - ctx.conn.write('HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n$ctx.headers\r\n\r\n$s') or { panic(err) } + ctx.done = true + ctx.conn.send_string(HTTP_404) or { return } } -pub fn (ctx Context) redirect(url string) { - if ctx.done { return } - ctx.conn.write('HTTP/1.1 302 Found\r\nLocation: $url\r\n$ctx.headers\r\n\r\n') or { panic(err) } -} - -pub fn (ctx Context) not_found(s string) { - if ctx.done { return } - ctx.conn.write(HTTP_404) or { panic(err) } -} - -pub fn (ctx mut Context) set_cookie(key, val string) { // TODO support directives, escape cookie value (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) +pub fn (ctx mut Context) set_cookie(key, val string) { + // TODO support directives, escape cookie value (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) //println('Set-Cookie $key=$val') - ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly') + ctx.add_header('Set-Cookie', '${key}=${val}; Secure; HttpOnly') } pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor @@ -92,8 +107,7 @@ pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor fn (ctx mut Context) add_header(key, val string) { //println('add_header($key, $val)') - ctx.headers = ctx.headers + - if ctx.headers == '' { '$key: $val' } else { '\r\n$key: $val' } + ctx.headers = ctx.headers + '\r\n$key: $val' //println(ctx.headers) } @@ -108,16 +122,14 @@ pub fn run(app mut T, port int) { //mut app := T{} app.init() for { - conn := l.accept() or { - panic('accept() failed') - } + conn := l.accept() or { panic('accept() failed') } //foobar() // TODO move this to handle_conn(conn, app) first_line:= conn.read_line() if first_line == '' { - conn.write(HTTP_500) or {} + conn.send_string(HTTP_500) or {} conn.close() or {} - return + continue } // Parse the first line // "GET / HTTP/1.1" @@ -125,18 +137,17 @@ pub fn run(app mut T, port int) { vals := first_line.split(' ') if vals.len < 2 { println('no vals for http') - conn.write(HTTP_500) or {} + conn.send_string(HTTP_500) or {} conn.close() or {} - return + continue } mut headers := []string - for _ in 0..30 { + for { + if headers.len >= 30 { break } header := conn.read_line() headers << header //println('header="$header" len = ' + header.len.str()) - if header.len <= 2 { - break - } + if header.len <= 2 { break } } mut action := vals[1][1..].all_before('/') if action.contains('?') { @@ -196,9 +207,11 @@ pub fn run(app mut T, port int) { // } // Call the right action - println('action=$action') + $if debug { + println('action=$action') + } app.$action() or { - conn.write(HTTP_404) or {} + conn.send_string(HTTP_404) or {} } conn.close() or {} reset := 'reset' @@ -267,6 +280,7 @@ fn (ctx mut Context) scan_static_directory(directory_path, mount_path string) { } pub fn (ctx mut Context) handle_static(directory_path string) bool { + if ctx.done { return false } ctx.scan_static_directory(directory_path, '') static_file := ctx.static_files[ctx.req.url] @@ -274,8 +288,7 @@ pub fn (ctx mut Context) handle_static(directory_path string) bool { if static_file != '' { data := os.read_file(static_file) or { return false } - ctx.conn.write('HTTP/1.1 200 OK\r\nContent-Type: $mime_type\r\n\r\n$data') or { panic(err) } - return true + return ctx.send_response_to_client(mime_type, data) } return false }