mirror of
https://github.com/vlang/v.git
synced 2025-08-03 09:47:15 -04:00
949 lines
28 KiB
V
949 lines
28 KiB
V
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
|
// Use of this source code is governed by an MIT license
|
|
// that can be found in the LICENSE file.
|
|
module veb
|
|
|
|
import io
|
|
import net
|
|
import net.http
|
|
import net.urllib
|
|
import os
|
|
import time
|
|
import strings
|
|
import picoev
|
|
|
|
// A type which doesn't get filtered inside templates
|
|
pub type RawHtml = string
|
|
|
|
// A dummy structure that returns from routes to indicate that you actually sent something to a user
|
|
@[noinit]
|
|
pub struct Result {}
|
|
|
|
// no_result does nothing, but returns `veb.Result`. Only use it when you are sure
|
|
// a response will be send over the connection, or in combination with `Context.takeover_conn`
|
|
pub fn no_result() Result {
|
|
return Result{}
|
|
}
|
|
|
|
struct Route {
|
|
methods []http.Method
|
|
path string
|
|
host string
|
|
mut:
|
|
middlewares []voidptr
|
|
after_middlewares []voidptr
|
|
}
|
|
|
|
// Generate route structs for an app
|
|
fn generate_routes[A, X](app &A) !map[string]Route {
|
|
// Parsing methods attributes
|
|
mut routes := map[string]Route{}
|
|
$for method in A.methods {
|
|
$if method.return_type is Result {
|
|
http_methods, route_path, host := parse_attrs(method.name, method.attrs) or {
|
|
return error('error parsing method attributes: ${err}')
|
|
}
|
|
|
|
mut route := Route{
|
|
methods: http_methods
|
|
path: route_path
|
|
host: host
|
|
}
|
|
|
|
$if A is MiddlewareApp {
|
|
route.middlewares = app.Middleware.get_handlers_for_route[X](route_path)
|
|
route.after_middlewares = app.Middleware.get_handlers_for_route_after[X](route_path)
|
|
}
|
|
|
|
routes[method.name] = route
|
|
}
|
|
}
|
|
return routes
|
|
}
|
|
|
|
// run - start a new veb server, listening to all available addresses, at the specified `port`
|
|
pub fn run[A, X](mut global_app A, port int) {
|
|
run_at[A, X](mut global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
|
|
}
|
|
|
|
@[params]
|
|
pub struct RunParams {
|
|
pub:
|
|
// use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
|
|
family net.AddrFamily = .ip6
|
|
host string
|
|
port int = default_port
|
|
show_startup_message bool = true
|
|
timeout_in_seconds int = 30
|
|
}
|
|
|
|
struct FileResponse {
|
|
pub mut:
|
|
open bool
|
|
file os.File
|
|
total i64
|
|
pos i64
|
|
should_close_conn bool
|
|
}
|
|
|
|
// close the open file and reset the struct to its default values
|
|
pub fn (mut fr FileResponse) done() {
|
|
fr.open = false
|
|
fr.file.close()
|
|
fr.total = 0
|
|
fr.pos = 0
|
|
fr.should_close_conn = false
|
|
}
|
|
|
|
struct StringResponse {
|
|
pub mut:
|
|
open bool
|
|
str string
|
|
pos i64
|
|
should_close_conn bool
|
|
}
|
|
|
|
// free the current string and reset the struct to its default values
|
|
@[manualfree]
|
|
pub fn (mut sr StringResponse) done() {
|
|
sr.open = false
|
|
sr.pos = 0
|
|
sr.should_close_conn = false
|
|
unsafe { sr.str.free() }
|
|
}
|
|
|
|
// EV context
|
|
struct RequestParams {
|
|
global_app voidptr
|
|
controllers []&ControllerPath
|
|
routes &map[string]Route
|
|
timeout_in_seconds int
|
|
mut:
|
|
// request body buffer
|
|
buf &u8 = unsafe { nil }
|
|
// idx keeps track of how much of the request body has been read
|
|
// for each incomplete request, see `handle_conn`
|
|
idx []int
|
|
incomplete_requests []http.Request
|
|
file_responses []FileResponse
|
|
string_responses []StringResponse
|
|
}
|
|
|
|
// reset request parameters for `fd`:
|
|
// reset content-length index and the http request
|
|
pub fn (mut params RequestParams) request_done(fd int) {
|
|
params.incomplete_requests[fd] = http.Request{}
|
|
params.idx[fd] = 0
|
|
}
|
|
|
|
interface BeforeAcceptApp {
|
|
mut:
|
|
before_accept_loop()
|
|
}
|
|
|
|
// run_at - start a new veb server, listening only on a specific address `host`, at the specified `port`
|
|
// Example: veb.run_at(new_app(), veb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
|
|
@[direct_array_access; manualfree]
|
|
pub fn run_at[A, X](mut global_app A, params RunParams) ! {
|
|
if params.port <= 0 || params.port > 65535 {
|
|
return error('invalid port number `${params.port}`, it should be between 1 and 65535')
|
|
}
|
|
|
|
routes := generate_routes[A, X](global_app)!
|
|
controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)!
|
|
|
|
if params.show_startup_message {
|
|
host := if params.host == '' { 'localhost' } else { params.host }
|
|
println('[veb] Running app on http://${host}:${params.port}/')
|
|
}
|
|
flush_stdout()
|
|
|
|
mut pico_context := &RequestParams{
|
|
global_app: unsafe { global_app }
|
|
controllers: controllers_sorted
|
|
routes: &routes
|
|
timeout_in_seconds: params.timeout_in_seconds
|
|
}
|
|
|
|
pico_context.idx = []int{len: picoev.max_fds}
|
|
// reserve space for read and write buffers
|
|
pico_context.buf = unsafe { malloc_noscan(picoev.max_fds * max_read + 1) }
|
|
defer {
|
|
unsafe {
|
|
free(pico_context.buf)
|
|
}
|
|
}
|
|
pico_context.incomplete_requests = []http.Request{len: picoev.max_fds}
|
|
pico_context.file_responses = []FileResponse{len: picoev.max_fds}
|
|
pico_context.string_responses = []StringResponse{len: picoev.max_fds}
|
|
|
|
mut pico := picoev.new(
|
|
port: params.port
|
|
raw_cb: ev_callback[A, X]
|
|
user_data: pico_context
|
|
timeout_secs: params.timeout_in_seconds
|
|
family: params.family
|
|
host: params.host
|
|
)!
|
|
|
|
$if A is BeforeAcceptApp {
|
|
global_app.before_accept_loop()
|
|
}
|
|
|
|
// Forever accept every connection that comes
|
|
pico.serve()
|
|
}
|
|
|
|
@[direct_array_access]
|
|
fn ev_callback[A, X](mut pv picoev.Picoev, fd int, events int) {
|
|
mut params := unsafe { &RequestParams(pv.user_data) }
|
|
|
|
if events == picoev.picoev_timeout {
|
|
$if trace_picoev_callback ? {
|
|
eprintln('> request timeout on file descriptor ${fd}')
|
|
}
|
|
|
|
handle_timeout(mut pv, mut params, fd)
|
|
} else if events == picoev.picoev_write {
|
|
$if trace_picoev_callback ? {
|
|
eprintln('> write event on file descriptor ${fd}')
|
|
}
|
|
|
|
if params.file_responses[fd].open {
|
|
handle_write_file(mut pv, mut params, fd)
|
|
} else if params.string_responses[fd].open {
|
|
handle_write_string(mut pv, mut params, fd)
|
|
} else {
|
|
// This should never happen, but it does on pages, that refer to static resources,
|
|
// in folders, added with `mount_static_folder_at`. See also
|
|
// https://github.com/vlang/edu-platform/blob/0c203f0384cf24f917f9a7c9bb150f8d64aca00f/main.v#L92
|
|
$if debug_ev_callback ? {
|
|
eprintln('[veb] error: write event on connection should be closed')
|
|
}
|
|
pv.close_conn(fd)
|
|
}
|
|
} else if events == picoev.picoev_read {
|
|
$if trace_picoev_callback ? {
|
|
eprintln('> read event on file descriptor ${fd}')
|
|
}
|
|
// println('ev_callback fd=${fd} params.routes=${params.routes.len}')
|
|
handle_read[A, X](mut pv, mut params, fd)
|
|
} else {
|
|
// should never happen
|
|
eprintln('[veb] error: invalid picoev event ${events}')
|
|
}
|
|
}
|
|
|
|
fn handle_timeout(mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|
mut conn := &net.TcpConn{
|
|
sock: net.tcp_socket_from_handle_raw(fd)
|
|
handle: fd
|
|
is_blocking: false
|
|
}
|
|
|
|
fast_send_resp(mut conn, http_408) or {}
|
|
pv.close_conn(fd)
|
|
|
|
params.request_done(fd)
|
|
}
|
|
|
|
// handle_write_file reads data from a file and sends that data over the socket.
|
|
@[direct_array_access; manualfree]
|
|
fn handle_write_file(mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|
mut bytes_to_write := int(params.file_responses[fd].total - params.file_responses[fd].pos)
|
|
|
|
$if linux || freebsd {
|
|
bytes_written := sendfile(fd, params.file_responses[fd].file.fd, bytes_to_write)
|
|
if bytes_written < 0 {
|
|
params.file_responses[fd].pos += bytes_to_write
|
|
} else {
|
|
params.file_responses[fd].pos += bytes_written
|
|
}
|
|
} $else {
|
|
if bytes_to_write > max_write {
|
|
bytes_to_write = max_write
|
|
}
|
|
|
|
data := unsafe { malloc(bytes_to_write) }
|
|
defer {
|
|
unsafe { free(data) }
|
|
}
|
|
|
|
mut conn := &net.TcpConn{
|
|
sock: net.tcp_socket_from_handle_raw(fd)
|
|
handle: fd
|
|
is_blocking: false
|
|
write_timeout: params.timeout_in_seconds * time.second
|
|
}
|
|
|
|
params.file_responses[fd].file.read_into_ptr(data, bytes_to_write) or {
|
|
params.file_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
return
|
|
}
|
|
actual_written := send_string_ptr(mut conn, data, bytes_to_write) or {
|
|
params.file_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
return
|
|
}
|
|
params.file_responses[fd].pos += actual_written
|
|
}
|
|
|
|
if params.file_responses[fd].pos == params.file_responses[fd].total {
|
|
// file is done writing
|
|
params.file_responses[fd].done()
|
|
handle_complete_request(params.file_responses[fd].should_close_conn, mut pv, fd)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handle_write_string reads data from a string and sends that data over the socket
|
|
@[direct_array_access]
|
|
fn handle_write_string(mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|
mut bytes_to_write := int(params.string_responses[fd].str.len - params.string_responses[fd].pos)
|
|
|
|
if bytes_to_write > max_write {
|
|
bytes_to_write = max_write
|
|
}
|
|
|
|
mut conn := &net.TcpConn{
|
|
sock: net.tcp_socket_from_handle_raw(fd)
|
|
handle: fd
|
|
is_blocking: false
|
|
}
|
|
|
|
// pointer magic to start at the correct position in the buffer
|
|
data := unsafe { params.string_responses[fd].str.str + params.string_responses[fd].pos }
|
|
actual_written := send_string_ptr(mut conn, data, bytes_to_write) or {
|
|
params.string_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
return
|
|
}
|
|
params.string_responses[fd].pos += actual_written
|
|
if params.string_responses[fd].pos == params.string_responses[fd].str.len {
|
|
// done writing
|
|
params.string_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
handle_complete_request(params.string_responses[fd].should_close_conn, mut pv,
|
|
fd)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handle_read reads data from the connection and if the request is complete
|
|
// it calls `handle_route` and closes the connection.
|
|
// If the request is not complete, it stores the incomplete request in `params`
|
|
// and the connection stays open until it is ready to read again
|
|
@[direct_array_access; manualfree]
|
|
fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|
// println('handle_read() fd=${fd} params.routes=${params.routes}')
|
|
mut conn := &net.TcpConn{
|
|
sock: net.tcp_socket_from_handle_raw(fd)
|
|
handle: fd
|
|
is_blocking: false
|
|
}
|
|
|
|
// cap the max_read to 8KB
|
|
mut reader := io.new_buffered_reader(reader: conn, cap: max_read)
|
|
defer {
|
|
unsafe {
|
|
reader.free()
|
|
}
|
|
}
|
|
|
|
// take the previous incomplete request
|
|
mut req := params.incomplete_requests[fd]
|
|
|
|
// check if there is an incomplete request for this file descriptor
|
|
if params.idx[fd] == 0 {
|
|
// set the read and write timeout according to picoev settings when the
|
|
// connection is first encountered
|
|
conn.set_read_timeout(params.timeout_in_seconds)
|
|
conn.set_write_timeout(params.timeout_in_seconds)
|
|
// first time that this connection is being read from, so we parse the
|
|
// request header first
|
|
req = http.parse_request_head(mut reader) or {
|
|
// Prevents errors from being thrown when BufferedReader is empty
|
|
if err !is io.Eof {
|
|
eprintln('[veb] error parsing request: ${err}')
|
|
}
|
|
// the buffered reader was empty meaning that the client probably
|
|
// closed the connection.
|
|
pv.close_conn(fd)
|
|
params.incomplete_requests[fd] = http.Request{}
|
|
return
|
|
}
|
|
if reader.total_read >= max_read {
|
|
// throw an error when the request header is larger than 8KB
|
|
// same limit that apache handles
|
|
eprintln('[veb] error parsing request: too large')
|
|
fast_send_resp(mut conn, http_413) or {}
|
|
pv.close_conn(fd)
|
|
params.incomplete_requests[fd] = http.Request{}
|
|
return
|
|
}
|
|
}
|
|
|
|
// check if the request has a body
|
|
content_length := req.header.get(.content_length) or { '0' }
|
|
if content_length.int() > 0 {
|
|
mut max_bytes_to_read := max_read - reader.total_read
|
|
mut bytes_to_read := content_length.int() - params.idx[fd]
|
|
// cap the bytes to read to 8KB for the body, including the request headers if any
|
|
if bytes_to_read > max_read - reader.total_read {
|
|
bytes_to_read = max_read - reader.total_read
|
|
}
|
|
|
|
mut buf_ptr := params.buf
|
|
unsafe {
|
|
buf_ptr += fd * max_read // pointer magic
|
|
}
|
|
// convert to []u8 for BufferedReader
|
|
mut buf := unsafe { buf_ptr.vbytes(max_bytes_to_read) }
|
|
|
|
n := reader.read(mut buf) or {
|
|
if reader.total_read > 0 {
|
|
// the headers were parsed in this cycle, but the body has not been
|
|
// sent yet. No need to error
|
|
return
|
|
}
|
|
|
|
eprintln('[veb] error reading request body: ${err}')
|
|
|
|
if err is io.Eof {
|
|
// we expect more data to be send, but an Eof error occurred, meaning
|
|
// that there is no more data to be read from the socket.
|
|
// And at this point we expect that there is data to be read for the body.
|
|
fast_send_resp(mut conn, http.new_response(
|
|
status: .bad_request
|
|
body: 'Mismatch of body length and Content-Length header'
|
|
header: http.new_header(
|
|
key: .content_type
|
|
value: 'text/plain'
|
|
).join(headers_close)
|
|
)) or {}
|
|
}
|
|
|
|
pv.close_conn(fd)
|
|
params.incomplete_requests[fd] = http.Request{}
|
|
params.idx[fd] = 0
|
|
return
|
|
}
|
|
|
|
// there is no more data to be sent, but it is less than the Content-Length header
|
|
// so it is a mismatch of body length and content length.
|
|
// Or if there is more data received then the Content-Length header specified
|
|
if (n == 0 && params.idx[fd] != 0) || params.idx[fd] + n > content_length.int() {
|
|
fast_send_resp(mut conn, http.new_response(
|
|
status: .bad_request
|
|
body: 'Mismatch of body length and Content-Length header'
|
|
header: http.new_header(
|
|
key: .content_type
|
|
value: 'text/plain'
|
|
).join(headers_close)
|
|
)) or {}
|
|
|
|
pv.close_conn(fd)
|
|
params.incomplete_requests[fd] = http.Request{}
|
|
params.idx[fd] = 0
|
|
return
|
|
} else if n < bytes_to_read || params.idx[fd] + n < content_length.int() {
|
|
// request is incomplete wait until the socket becomes ready to read again
|
|
params.idx[fd] += n
|
|
// TODO: change this to a memcpy function?
|
|
req.data += buf[0..n].bytestr()
|
|
params.incomplete_requests[fd] = req
|
|
return
|
|
} else {
|
|
// request is complete: n = bytes_to_read
|
|
params.idx[fd] += n
|
|
req.data += buf[0..n].bytestr()
|
|
}
|
|
}
|
|
|
|
defer {
|
|
params.request_done(fd)
|
|
}
|
|
|
|
if completed_context := handle_request[A, X](mut conn, req, params) {
|
|
if completed_context.takeover {
|
|
// the connection should be kept open, but removed from the picoev loop.
|
|
// This way veb can continue handling other connections and the user can
|
|
// keep the connection open indefinitely
|
|
pv.delete(fd)
|
|
return
|
|
}
|
|
|
|
// TODO: At this point the Context can safely be freed when this function returns.
|
|
// The user will have to clone the context if the context object should be kept.
|
|
// defer {
|
|
// completed_context.free()
|
|
// }
|
|
|
|
match completed_context.return_type {
|
|
.normal {
|
|
// small optimization: if the response is small write it immediately
|
|
// the socket is most likely able to write all the data without blocking.
|
|
// See Context.send_file for why we use max_read instead of max_write.
|
|
if completed_context.res.body.len < max_read {
|
|
fast_send_resp(mut conn, completed_context.res) or {}
|
|
handle_complete_request(completed_context.client_wants_to_close, mut
|
|
pv, fd)
|
|
} else {
|
|
params.string_responses[fd].open = true
|
|
params.string_responses[fd].str = completed_context.res.body
|
|
res := pv.add(fd, picoev.picoev_write, params.timeout_in_seconds,
|
|
picoev.raw_callback)
|
|
// picoev error
|
|
if res == -1 {
|
|
// should not happen
|
|
params.string_responses[fd].done()
|
|
fast_send_resp(mut conn, http_500) or {}
|
|
handle_complete_request(completed_context.client_wants_to_close, mut
|
|
pv, fd)
|
|
return
|
|
}
|
|
// no errors we can send the HTTP headers
|
|
fast_send_resp_header(mut conn, completed_context.res) or {}
|
|
}
|
|
}
|
|
.file {
|
|
// save file information
|
|
length := completed_context.res.header.get(.content_length) or {
|
|
fast_send_resp(mut conn, http_500) or {}
|
|
return
|
|
}
|
|
params.file_responses[fd].total = length.i64()
|
|
params.file_responses[fd].file = os.open(completed_context.return_file) or {
|
|
// Context checks if the file is valid, so this should never happen
|
|
fast_send_resp(mut conn, http_500) or {}
|
|
params.file_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
return
|
|
}
|
|
params.file_responses[fd].open = true
|
|
|
|
res := pv.add(fd, picoev.picoev_write, params.timeout_in_seconds, picoev.raw_callback)
|
|
// picoev error
|
|
if res == -1 {
|
|
// should not happen
|
|
fast_send_resp(mut conn, http_500) or {}
|
|
params.file_responses[fd].done()
|
|
pv.close_conn(fd)
|
|
return
|
|
}
|
|
// no errors we can send the HTTP headers
|
|
fast_send_resp_header(mut conn, completed_context.res) or {}
|
|
}
|
|
}
|
|
} else {
|
|
// invalid request headers/data
|
|
pv.close_conn(fd)
|
|
}
|
|
}
|
|
|
|
// close the connection when `should_close` is true.
|
|
@[inline]
|
|
fn handle_complete_request(should_close bool, mut pv picoev.Picoev, fd int) {
|
|
if should_close {
|
|
pv.close_conn(fd)
|
|
}
|
|
}
|
|
|
|
fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestParams) ?&Context {
|
|
// println('handle_request() params.routes=${params.routes}')
|
|
mut global_app := unsafe { &A(params.global_app) }
|
|
|
|
// TODO: change this variable to include the total wait time over each network cycle
|
|
// maybe store it in Request.user_ptr ?
|
|
page_gen_start := time.ticks()
|
|
|
|
$if trace_request ? {
|
|
dump(req)
|
|
}
|
|
$if trace_request_url ? {
|
|
dump(req.url)
|
|
}
|
|
|
|
// parse the URL, query and form data
|
|
mut url := urllib.parse(req.url) or {
|
|
eprintln('[veb] error parsing path "${req.url}": ${err}')
|
|
return none
|
|
}
|
|
query := parse_query_from_url(url)
|
|
form, files := parse_form_from_request(req) or {
|
|
// Bad request
|
|
eprintln('[veb] error parsing form: ${err.msg()}')
|
|
conn.write(http_400.bytes()) or {}
|
|
return none
|
|
}
|
|
|
|
// remove the port from the HTTP Host header
|
|
host_with_port := req.header.get(.host) or { '' }
|
|
host, _ := urllib.split_host_port(host_with_port)
|
|
|
|
// Create Context with request data
|
|
mut ctx := &Context{
|
|
req: req
|
|
page_gen_start: page_gen_start
|
|
conn: conn
|
|
query: query
|
|
form: form
|
|
files: files
|
|
}
|
|
|
|
if connection_header := req.header.get(.connection) {
|
|
// A client that does not support persistent connections MUST send the
|
|
// "close" connection option in every request message.
|
|
if connection_header.to_lower() == 'close' {
|
|
ctx.client_wants_to_close = true
|
|
}
|
|
}
|
|
|
|
$if A is StaticApp {
|
|
ctx.custom_mime_types = global_app.static_mime_types.clone()
|
|
}
|
|
|
|
// match controller paths
|
|
$if A is ControllerInterface {
|
|
if completed_context := handle_controllers[X](params.controllers, ctx, mut url,
|
|
host)
|
|
{
|
|
return completed_context
|
|
}
|
|
}
|
|
|
|
// create a new user context and pass the veb's context
|
|
mut user_context := X{}
|
|
user_context.Context = ctx
|
|
|
|
handle_route[A, X](mut global_app, mut user_context, url, host, params.routes)
|
|
// we need to explicitly tell the V compiler to return a reference
|
|
return &user_context.Context
|
|
}
|
|
|
|
fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string, routes &map[string]Route) {
|
|
// println('\n\nhandle_route() url=${url} routes=${routes}')
|
|
mut route := Route{}
|
|
mut middleware_has_sent_response := false
|
|
mut not_found := false
|
|
|
|
defer {
|
|
// execute middleware functions after veb is done and before the response is send
|
|
mut was_done := true
|
|
$if A is MiddlewareApp {
|
|
if !not_found && !middleware_has_sent_response {
|
|
// if the middleware doesn't send an alternate response, but only changes the
|
|
// response object we only have to check if the `done` was previously set to true
|
|
was_done = user_context.Context.done
|
|
// reset `done` so the middleware functions can return a different response
|
|
// 1 time only, since the `done` guard is still present in
|
|
// `Context.send_response_to_client`
|
|
user_context.Context.done = false
|
|
|
|
// no need to check the result of `validate_middleware`, since a response has to be sent
|
|
// anyhow. This function makes sure no further middleware is executed.
|
|
validate_middleware[X](mut user_context, app.Middleware.get_global_handlers_after[X]())
|
|
validate_middleware[X](mut user_context, route.after_middlewares)
|
|
}
|
|
}
|
|
// send only the headers, because if the response body is too big, TcpConn code will
|
|
// actually block, because it has to wait for the socket to become ready to write. veb
|
|
// will handle this case.
|
|
if !was_done && !user_context.Context.done && !user_context.Context.takeover {
|
|
eprintln('[veb] handler for route "${url.path}" does not send any data!')
|
|
// send response anyway so the connection won't block
|
|
// fast_send_resp_header(mut user_context.conn, user_context.res) or {}
|
|
} else if !user_context.Context.takeover {
|
|
// fast_send_resp_header(mut user_context.conn, user_context.res) or {}
|
|
}
|
|
// Context.takeover is set to true, so the user must close the connection and sent a response.
|
|
}
|
|
|
|
url_words := url.path.split('/').filter(it != '')
|
|
|
|
$if veb_livereload ? {
|
|
if url.path.starts_with('/veb_livereload/') {
|
|
if url.path.ends_with('current') {
|
|
user_context.handle_veb_livereload_current()
|
|
return
|
|
}
|
|
if url.path.ends_with('script.js') {
|
|
user_context.handle_veb_livereload_script()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// first execute before_request
|
|
$if A is HasBeforeRequest {
|
|
app.before_request()
|
|
}
|
|
// user_context.before_request()
|
|
if user_context.Context.done {
|
|
return
|
|
}
|
|
|
|
// then execute global middleware functions
|
|
$if A is MiddlewareApp {
|
|
if validate_middleware[X](mut user_context, app.Middleware.get_global_handlers[X]()) == false {
|
|
middleware_has_sent_response = true
|
|
return
|
|
}
|
|
}
|
|
|
|
$if A is StaticApp {
|
|
if serve_if_static[A, X](app, mut user_context, url, host) {
|
|
// successfully served a static file
|
|
return
|
|
}
|
|
}
|
|
|
|
// Route matching and match route specific middleware as last step
|
|
$for method in A.methods {
|
|
$if method.return_type is Result {
|
|
route = (*routes)[method.name] or {
|
|
eprintln('[veb] parsed attributes for the `${method.name}` are not found, skipping...')
|
|
Route{}
|
|
}
|
|
|
|
// Skip if the HTTP request method does not match the attributes
|
|
if user_context.Context.req.method in route.methods {
|
|
// Used for route matching
|
|
route_words := route.path.split('/').filter(it != '')
|
|
|
|
// Skip if the host does not match or is empty
|
|
if route.host == '' || route.host == host {
|
|
can_have_data_args := user_context.Context.req.method == .post
|
|
|| user_context.Context.req.method == .get
|
|
// Route immediate matches first
|
|
// For example URL `/register` matches route `/:user`, but `fn register()`
|
|
// should be called first.
|
|
if !route.path.contains('/:') && url_words == route_words {
|
|
// We found a match
|
|
$if A is MiddlewareApp {
|
|
if validate_middleware[X](mut user_context, route.middlewares) == false {
|
|
middleware_has_sent_response = true
|
|
return
|
|
}
|
|
}
|
|
|
|
if method.args.len > 1 && can_have_data_args {
|
|
// Populate method args with form or query values
|
|
mut args := []string{cap: method.args.len + 1}
|
|
data := if user_context.Context.req.method == .get {
|
|
user_context.Context.query
|
|
} else {
|
|
user_context.Context.form
|
|
}
|
|
|
|
for param in method.args[1..] {
|
|
args << data[param.name]
|
|
}
|
|
|
|
// println('m1')
|
|
app.$method(mut user_context, args)
|
|
} else {
|
|
// println('m2')
|
|
app.$method(mut user_context)
|
|
}
|
|
return
|
|
}
|
|
|
|
// println('route_words=${route_words} method=${method}')
|
|
if url_words.len == 0 && route_words == ['index'] && method.name == 'index' {
|
|
$if A is MiddlewareApp {
|
|
if validate_middleware[X](mut user_context, route.middlewares) == false {
|
|
middleware_has_sent_response = true
|
|
return
|
|
}
|
|
}
|
|
|
|
if method.args.len > 1 && can_have_data_args {
|
|
// Populate method args with form or query values
|
|
mut args := []string{cap: method.args.len + 1}
|
|
|
|
data := if user_context.Context.req.method == .get {
|
|
user_context.Context.query
|
|
} else {
|
|
user_context.Context.form
|
|
}
|
|
|
|
for param in method.args[1..] {
|
|
args << data[param.name]
|
|
}
|
|
|
|
// println('m3')
|
|
app.$method(mut user_context, args)
|
|
} else {
|
|
// println('m4')
|
|
app.$method(mut user_context)
|
|
}
|
|
return
|
|
}
|
|
|
|
if params := route_matches(url_words, route_words) {
|
|
$if A is MiddlewareApp {
|
|
if validate_middleware[X](mut user_context, route.middlewares) == false {
|
|
middleware_has_sent_response = true
|
|
return
|
|
}
|
|
}
|
|
|
|
method_args := params.clone()
|
|
if method_args.len + 1 != method.args.len {
|
|
eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})')
|
|
}
|
|
// println('m5')
|
|
app.$method(mut user_context, method_args)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// return 404
|
|
user_context.not_found()
|
|
not_found = true
|
|
return
|
|
}
|
|
|
|
fn route_matches(url_words []string, route_words []string) ?[]string {
|
|
// println('route_matches(url_words:${url_words} route_words:${route_words}')
|
|
// URL path should be at least as long as the route path
|
|
// except for the catchall route (`/:path...`)
|
|
if route_words.len == 1 && route_words[0].starts_with(':') && route_words[0].ends_with('...') {
|
|
return ['/' + url_words.join('/')]
|
|
}
|
|
if url_words.len < route_words.len {
|
|
return none
|
|
}
|
|
|
|
mut params := []string{cap: url_words.len}
|
|
if url_words.len == route_words.len {
|
|
for i in 0 .. url_words.len {
|
|
if route_words[i].starts_with(':') {
|
|
// We found a path parameter
|
|
params << url_words[i]
|
|
} else if route_words[i] != url_words[i] {
|
|
// This url does not match the route
|
|
return none
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
// The last route can end with ... indicating an array
|
|
if route_words.len == 0 || !route_words[route_words.len - 1].ends_with('...') {
|
|
return none
|
|
}
|
|
|
|
for i in 0 .. route_words.len - 1 {
|
|
if route_words[i].starts_with(':') {
|
|
// We found a path parameter
|
|
params << url_words[i]
|
|
} else if route_words[i] != url_words[i] {
|
|
// This url does not match the route
|
|
return none
|
|
}
|
|
}
|
|
params << url_words[route_words.len - 1..url_words.len].join('/')
|
|
return params
|
|
}
|
|
|
|
// check if request is for a static file and serves it
|
|
// returns true if we served a static file, false otherwise
|
|
@[manualfree]
|
|
fn serve_if_static[A, X](app &A, mut user_context X, url urllib.URL, host string) bool {
|
|
// TODO: handle url parameters properly - for now, ignore them
|
|
mut asked_path := url.path
|
|
base_path := os.base(asked_path)
|
|
|
|
if !base_path.contains('.') && !asked_path.ends_with('/') {
|
|
asked_path += '/'
|
|
}
|
|
|
|
if asked_path.ends_with('/') {
|
|
if app.static_files[asked_path + 'index.html'] != '' {
|
|
asked_path += 'index.html'
|
|
} else if app.static_files[asked_path + 'index.htm'] != '' {
|
|
asked_path += 'index.htm'
|
|
}
|
|
}
|
|
static_file := app.static_files[asked_path] or { return false }
|
|
|
|
// StaticHandler ensures that the mime type exists on either the App or in veb
|
|
ext := os.file_ext(static_file).to_lower()
|
|
mut mime_type := app.static_mime_types[ext] or { mime_types[ext] }
|
|
|
|
static_host := app.static_hosts[asked_path] or { '' }
|
|
if static_file == '' || mime_type == '' {
|
|
return false
|
|
}
|
|
if static_host != '' && static_host != host {
|
|
return false
|
|
}
|
|
|
|
user_context.send_file(mime_type, static_file)
|
|
return true
|
|
}
|
|
|
|
// send a string over `conn`
|
|
fn send_string(mut conn net.TcpConn, s string) ! {
|
|
$if trace_send_string_conn ? {
|
|
eprintln('> send_string: conn: ${ptr_str(conn)}')
|
|
}
|
|
$if trace_response ? {
|
|
eprintln('> send_string:\n${s}\n')
|
|
}
|
|
if voidptr(conn) == unsafe { nil } {
|
|
return error('connection was closed before send_string')
|
|
}
|
|
conn.write_string(s)!
|
|
}
|
|
|
|
// send a string ptr over `conn`
|
|
fn send_string_ptr(mut conn net.TcpConn, ptr &u8, len int) !int {
|
|
$if trace_send_string_conn ? {
|
|
eprintln('> send_string: conn: ${ptr_str(conn)}')
|
|
}
|
|
// $if trace_response ? {
|
|
// eprintln('> send_string:\n${s}\n')
|
|
// }
|
|
if voidptr(conn) == unsafe { nil } {
|
|
return error('connection was closed before send_string')
|
|
}
|
|
return conn.write_ptr(ptr, len)
|
|
}
|
|
|
|
fn fast_send_resp_header(mut conn net.TcpConn, resp http.Response) ! {
|
|
mut sb := strings.new_builder(resp.body.len + 200)
|
|
sb.write_string('HTTP/')
|
|
sb.write_string(resp.http_version)
|
|
sb.write_string(' ')
|
|
sb.write_decimal(resp.status_code)
|
|
sb.write_string(' ')
|
|
sb.write_string(resp.status_msg)
|
|
sb.write_string('\r\n')
|
|
|
|
resp.header.render_into_sb(mut sb,
|
|
version: resp.version()
|
|
)
|
|
sb.write_string('\r\n')
|
|
send_string(mut conn, sb.str())!
|
|
}
|
|
|
|
// Formats resp to a string suitable for HTTP response transmission
|
|
// A fast version of `resp.bytestr()` used with
|
|
// `send_string(mut ctx.conn, resp.bytestr())`
|
|
fn fast_send_resp(mut conn net.TcpConn, resp http.Response) ! {
|
|
fast_send_resp_header(mut conn, resp)!
|
|
send_string(mut conn, resp.body)!
|
|
}
|
|
|
|
// Set s to the form error
|
|
pub fn (mut ctx Context) error(s string) {
|
|
eprintln('[veb] Context.error: ${s}')
|
|
ctx.form_error = s
|
|
}
|