refactor(web): simplified web framework in general

Jef Roosens 2022-08-12 14:39:42 +02:00
parent 78b0918df7
commit 3a73ea0632
Signed by untrusted user: Jef Roosens
GPG Key ID: B75D4F293C7052DB
2 changed files with 209 additions and 191 deletions

125
src/web/consts.v 100644
View File

@ -0,0 +1,125 @@
module web
import net.http
// A dummy structure that returns from routes to indicate that you actually sent something to a user
[noinit]
pub struct Result {}
pub const (
methods_with_form = [http.Method.post, .put, .patch]
headers_close = http.new_custom_header_from_map({
'Server': 'VWeb'
http.CommonHeader.connection.str(): 'close'
}) or { panic('should never fail') }
http_302 = http.new_response(
status: .found
body: '302 Found'
header: headers_close
)
http_400 = http.new_response(
status: .bad_request
body: '400 Bad Request'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
http_404 = http.new_response(
status: .not_found
body: '404 Not Found'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
http_500 = http.new_response(
status: .internal_server_error
body: '500 Internal Server Error'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
mime_types = {
'.aac': 'audio/aac'
'.abw': 'application/x-abiword'
'.arc': 'application/x-freearc'
'.avi': 'video/x-msvideo'
'.azw': 'application/vnd.amazon.ebook'
'.bin': 'application/octet-stream'
'.bmp': 'image/bmp'
'.bz': 'application/x-bzip'
'.bz2': 'application/x-bzip2'
'.cda': 'application/x-cdf'
'.csh': 'application/x-csh'
'.css': 'text/css'
'.csv': 'text/csv'
'.doc': 'application/msword'
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
'.eot': 'application/vnd.ms-fontobject'
'.epub': 'application/epub+zip'
'.gz': 'application/gzip'
'.gif': 'image/gif'
'.htm': 'text/html'
'.html': 'text/html'
'.ico': 'image/vnd.microsoft.icon'
'.ics': 'text/calendar'
'.jar': 'application/java-archive'
'.jpeg': 'image/jpeg'
'.jpg': 'image/jpeg'
'.js': 'text/javascript'
'.json': 'application/json'
'.jsonld': 'application/ld+json'
'.mid': 'audio/midi audio/x-midi'
'.midi': 'audio/midi audio/x-midi'
'.mjs': 'text/javascript'
'.mp3': 'audio/mpeg'
'.mp4': 'video/mp4'
'.mpeg': 'video/mpeg'
'.mpkg': 'application/vnd.apple.installer+xml'
'.odp': 'application/vnd.oasis.opendocument.presentation'
'.ods': 'application/vnd.oasis.opendocument.spreadsheet'
'.odt': 'application/vnd.oasis.opendocument.text'
'.oga': 'audio/ogg'
'.ogv': 'video/ogg'
'.ogx': 'application/ogg'
'.opus': 'audio/opus'
'.otf': 'font/otf'
'.png': 'image/png'
'.pdf': 'application/pdf'
'.php': 'application/x-httpd-php'
'.ppt': 'application/vnd.ms-powerpoint'
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
'.rar': 'application/vnd.rar'
'.rtf': 'application/rtf'
'.sh': 'application/x-sh'
'.svg': 'image/svg+xml'
'.swf': 'application/x-shockwave-flash'
'.tar': 'application/x-tar'
'.tif': 'image/tiff'
'.tiff': 'image/tiff'
'.ts': 'video/mp2t'
'.ttf': 'font/ttf'
'.txt': 'text/plain'
'.vsd': 'application/vnd.visio'
'.wav': 'audio/wav'
'.weba': 'audio/webm'
'.webm': 'video/webm'
'.webp': 'image/webp'
'.woff': 'font/woff'
'.woff2': 'font/woff2'
'.xhtml': 'application/xhtml+xml'
'.xls': 'application/vnd.ms-excel'
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
'.xml': 'application/xml'
'.xul': 'application/vnd.mozilla.xul+xml'
'.zip': 'application/zip'
'.3gp': 'video/3gpp'
'.3g2': 'video/3gpp2'
'.7z': 'application/x-7z-compressed'
}
max_http_post_size = 1024 * 1024
default_port = 8080
)

View File

@ -12,146 +12,23 @@ import time
import json import json
import log import log
// A dummy structure that returns from routes to indicate that you actually sent something to a user
[noinit]
pub struct Result {}
pub const (
methods_with_form = [http.Method.post, .put, .patch]
headers_close = http.new_custom_header_from_map({
'Server': 'VWeb'
http.CommonHeader.connection.str(): 'close'
}) or { panic('should never fail') }
http_302 = http.new_response(
status: .found
body: '302 Found'
header: headers_close
)
http_400 = http.new_response(
status: .bad_request
body: '400 Bad Request'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
http_404 = http.new_response(
status: .not_found
body: '404 Not Found'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
http_500 = http.new_response(
status: .internal_server_error
body: '500 Internal Server Error'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
mime_types = {
'.aac': 'audio/aac'
'.abw': 'application/x-abiword'
'.arc': 'application/x-freearc'
'.avi': 'video/x-msvideo'
'.azw': 'application/vnd.amazon.ebook'
'.bin': 'application/octet-stream'
'.bmp': 'image/bmp'
'.bz': 'application/x-bzip'
'.bz2': 'application/x-bzip2'
'.cda': 'application/x-cdf'
'.csh': 'application/x-csh'
'.css': 'text/css'
'.csv': 'text/csv'
'.doc': 'application/msword'
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
'.eot': 'application/vnd.ms-fontobject'
'.epub': 'application/epub+zip'
'.gz': 'application/gzip'
'.gif': 'image/gif'
'.htm': 'text/html'
'.html': 'text/html'
'.ico': 'image/vnd.microsoft.icon'
'.ics': 'text/calendar'
'.jar': 'application/java-archive'
'.jpeg': 'image/jpeg'
'.jpg': 'image/jpeg'
'.js': 'text/javascript'
'.json': 'application/json'
'.jsonld': 'application/ld+json'
'.mid': 'audio/midi audio/x-midi'
'.midi': 'audio/midi audio/x-midi'
'.mjs': 'text/javascript'
'.mp3': 'audio/mpeg'
'.mp4': 'video/mp4'
'.mpeg': 'video/mpeg'
'.mpkg': 'application/vnd.apple.installer+xml'
'.odp': 'application/vnd.oasis.opendocument.presentation'
'.ods': 'application/vnd.oasis.opendocument.spreadsheet'
'.odt': 'application/vnd.oasis.opendocument.text'
'.oga': 'audio/ogg'
'.ogv': 'video/ogg'
'.ogx': 'application/ogg'
'.opus': 'audio/opus'
'.otf': 'font/otf'
'.png': 'image/png'
'.pdf': 'application/pdf'
'.php': 'application/x-httpd-php'
'.ppt': 'application/vnd.ms-powerpoint'
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
'.rar': 'application/vnd.rar'
'.rtf': 'application/rtf'
'.sh': 'application/x-sh'
'.svg': 'image/svg+xml'
'.swf': 'application/x-shockwave-flash'
'.tar': 'application/x-tar'
'.tif': 'image/tiff'
'.tiff': 'image/tiff'
'.ts': 'video/mp2t'
'.ttf': 'font/ttf'
'.txt': 'text/plain'
'.vsd': 'application/vnd.visio'
'.wav': 'audio/wav'
'.weba': 'audio/webm'
'.webm': 'video/webm'
'.webp': 'image/webp'
'.woff': 'font/woff'
'.woff2': 'font/woff2'
'.xhtml': 'application/xhtml+xml'
'.xls': 'application/vnd.ms-excel'
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
'.xml': 'application/xml'
'.xul': 'application/vnd.mozilla.xul+xml'
'.zip': 'application/zip'
'.3gp': 'video/3gpp'
'.3g2': 'video/3gpp2'
'.7z': 'application/x-7z-compressed'
}
max_http_post_size = 1024 * 1024
default_port = 8080
)
// The Context struct represents the Context which hold the HTTP request and response. // The Context struct represents the Context which hold the HTTP request and response.
// It has fields for the query, form, files. // It has fields for the query, form, files.
pub struct Context { pub struct Context {
mut:
content_type string = 'text/plain'
status http.Status = http.Status.ok
pub: pub:
// HTTP Request // HTTP Request
req http.Request req http.Request
// TODO Response // TODO Response
pub mut: pub mut:
done bool
// time.ticks() from start of web connection handle.
// You can use it to determine how much time is spent on your request.
page_gen_start i64
// TCP connection to client. // TCP connection to client.
// But beware, do not store it for further use, after request processing web will close connection. // But beware, do not store it for further use, after request processing web will close connection.
conn &net.TcpConn conn &net.TcpConn
// Gives access to a shared logger object
logger shared log.Log
// REQUEST
// time.ticks() from start of web connection handle.
// You can use it to determine how much time is spent on your request.
page_gen_start i64
static_files map[string]string static_files map[string]string
static_mime_types map[string]string static_mime_types map[string]string
// Map containing query params for the route. // Map containing query params for the route.
@ -161,14 +38,13 @@ pub mut:
form map[string]string form map[string]string
// Files from multipart-form. // Files from multipart-form.
files map[string][]http.FileData files map[string][]http.FileData
header http.Header // response headers
// ? It doesn't seem to be used anywhere
form_error string
// Allows reading the request body // Allows reading the request body
reader io.BufferedReader reader io.BufferedReader
// Gives access to a shared logger object // RESPONSE
logger shared log.Log status http.Status = http.Status.ok
content_type string = 'text/plain'
// response headers
header http.Header
} }
struct FileData { struct FileData {
@ -188,40 +64,68 @@ struct Route {
// Probably you can use it for check user session cookie or add header. // Probably you can use it for check user session cookie or add header.
pub fn (ctx Context) before_request() {} pub fn (ctx Context) before_request() {}
// send_string // send_string writes the given string to the TCP connection socket.
fn send_string(mut conn net.TcpConn, s string) ? { fn (mut ctx Context) send_string(s string) ? {
conn.write(s.bytes())? ctx.conn.write(s.bytes())?
} }
// send_response_to_client sends a response to the client // send_reader reads at most `size` bytes from the given reader & writes them
[manualfree] // to the TCP connection socket. Internally, a 10KB buffer is used, to avoid
pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { // having to store all bytes in memory at once.
if ctx.done { fn (mut ctx Context) send_reader(mut reader io.Reader, size u64) ? {
return false mut buf := []u8{len: 10_000}
mut bytes_left := size
// Repeat as long as the stream still has data
for bytes_left > 0 {
bytes_read := reader.read(mut buf)?
bytes_left -= u64(bytes_read)
mut to_write := bytes_read
for to_write > 0 {
// TODO don't just loop infinitely here
bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue }
to_write = to_write - bytes_written
} }
ctx.done = true }
}
// build header
header := http.new_header_from_map({
http.CommonHeader.content_type: mimetype
http.CommonHeader.content_length: res.len.str()
}).join(ctx.header)
// send_response_header constructs a valid HTTP response with an empty body &
// sends it to the client.
pub fn (mut ctx Context) send_response_header() ? {
mut resp := http.Response{ mut resp := http.Response{
header: header.join(web.headers_close) header: ctx.header.join(headers_close)
body: res
} }
resp.set_version(.v1_1) resp.set_version(.v1_1)
resp.set_status(ctx.status) resp.set_status(ctx.status)
send_string(mut ctx.conn, resp.bytestr()) or { return false } ctx.send_string(resp.bytestr())?
}
// send_response constructs the resulting HTTP response with the given body
// string & sends it to the client.
pub fn (mut ctx Context) send_response(res string) bool {
ctx.send_response_header() or { return false }
ctx.send_string(res) or { return false }
return true
}
// send_reader_response constructs the resulting HTTP response with the given
// body & streams the reader's contents to the client.
pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) bool {
ctx.send_response_header() or { return false }
ctx.send_reader(mut reader, size) or { return false }
return true return true
} }
// text responds to a request with some plaintext. // text responds to a request with some plaintext.
pub fn (mut ctx Context) text(status http.Status, s string) Result { pub fn (mut ctx Context) text(status http.Status, s string) Result {
ctx.status = status ctx.status = status
ctx.content_type = 'text/plain'
ctx.send_response_to_client('text/plain', s) ctx.send_response(s)
return Result{} return Result{}
} }
@ -229,9 +133,10 @@ pub fn (mut ctx Context) text(status http.Status, s string) Result {
// json<T> HTTP_OK with json_s as payload with content-type `application/json` // json<T> HTTP_OK with json_s as payload with content-type `application/json`
pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { pub fn (mut ctx Context) json<T>(status http.Status, j T) Result {
ctx.status = status ctx.status = status
ctx.content_type = 'application/json'
json_s := json.encode(j) json_s := json.encode(j)
ctx.send_response_to_client('application/json', json_s) ctx.send_response(json_s)
return Result{} return Result{}
} }
@ -239,10 +144,6 @@ pub fn (mut ctx Context) json<T>(status http.Status, j T) Result {
// file Response HTTP_OK with file as payload // file Response HTTP_OK with file as payload
// This function manually implements responses because it needs to stream the file contents // This function manually implements responses because it needs to stream the file contents
pub fn (mut ctx Context) file(f_path string) Result { pub fn (mut ctx Context) file(f_path string) Result {
if ctx.done {
return Result{}
}
if !os.is_file(f_path) { if !os.is_file(f_path) {
return ctx.not_found() return ctx.not_found()
} }
@ -266,7 +167,7 @@ pub fn (mut ctx Context) file(f_path string) Result {
// We open the file before sending the headers in case reading fails // We open the file before sending the headers in case reading fails
file_size := os.file_size(f_path) file_size := os.file_size(f_path)
file := os.open(f_path) or { mut file := os.open(f_path) or {
eprintln(err.msg()) eprintln(err.msg())
ctx.server_error(500) ctx.server_error(500)
return Result{} return Result{}
@ -279,32 +180,32 @@ pub fn (mut ctx Context) file(f_path string) Result {
}).join(ctx.header) }).join(ctx.header)
mut resp := http.Response{ mut resp := http.Response{
header: header.join(web.headers_close) header: header.join(headers_close)
} }
resp.set_version(.v1_1) resp.set_version(.v1_1)
resp.set_status(ctx.status) resp.set_status(ctx.status)
send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } ctx.send_string(resp.bytestr()) or { return Result{} }
ctx.send_reader(mut file, file_size) or { return Result{} }
mut buf := []u8{len: 1_000_000} // mut buf := []u8{len: 1_000_000}
mut bytes_left := file_size // mut bytes_left := file_size
// Repeat as long as the stream still has data // // Repeat as long as the stream still has data
for bytes_left > 0 { // for bytes_left > 0 {
// TODO check if just breaking here is safe // // TODO check if just breaking here is safe
bytes_read := file.read(mut buf) or { break } // bytes_read := file.read(mut buf) or { break }
bytes_left -= u64(bytes_read) // bytes_left -= u64(bytes_read)
mut to_write := bytes_read // mut to_write := bytes_read
for to_write > 0 { // for to_write > 0 {
// TODO don't just loop infinitely here // // TODO don't just loop infinitely here
bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } // bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue }
to_write = to_write - bytes_written // to_write = to_write - bytes_written
} // }
} // }
ctx.done = true
return Result{} return Result{}
} }
@ -319,23 +220,16 @@ pub fn (mut ctx Context) server_error(ecode int) Result {
$if debug { $if debug {
eprintln('> ctx.server_error ecode: $ecode') eprintln('> ctx.server_error ecode: $ecode')
} }
if ctx.done { ctx.send_string(http_500.bytestr()) or {}
return Result{}
}
send_string(mut ctx.conn, web.http_500.bytestr()) or {}
return Result{} return Result{}
} }
// redirect Redirect to an url // redirect Redirect to an url
pub fn (mut ctx Context) redirect(url string) Result { pub fn (mut ctx Context) redirect(url string) Result {
if ctx.done { mut resp := http_302
return Result{}
}
ctx.done = true
mut resp := web.http_302
resp.header = resp.header.join(ctx.header) resp.header = resp.header.join(ctx.header)
resp.header.add(.location, url) resp.header.add(.location, url)
send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } ctx.send_string(resp.bytestr()) or { return Result{} }
return Result{} return Result{}
} }
@ -532,7 +426,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) {
} }
} }
// Route not found // Route not found
conn.write(web.http_404.bytes()) or {} conn.write(http_404.bytes()) or {}
} }
// route_matches returns wether a route matches // route_matches returns wether a route matches
@ -597,7 +491,6 @@ pub fn (ctx &Context) ip() string {
// error Set s to the form error // error Set s to the form error
pub fn (mut ctx Context) error(s string) { pub fn (mut ctx Context) error(s string) {
println('web error: $s') println('web error: $s')
ctx.form_error = s
} }
// filter Do not delete. // filter Do not delete.