Files
agila_open_tool/tools/v/vlib/veb/veb_fasthttp.v
T

309 lines
10 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 fasthttp
import net.http
import strconv
import strings
import time
import net.urllib
struct RequestParams {
global_app voidptr
controllers_sorted []&ControllerPath
routes &map[string]Route
benchmark_page_generation bool
}
const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
pub fn run_at[A, X](mut global_app A, params RunParams) ! {
run_new[A, X](mut global_app, params)!
}
// run_new - start a new veb server using the parallel fasthttp backend.
pub fn run_new[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')
}
if ssl_enabled(params) {
maybe_init_server[A](mut global_app, new_server_without_lifecycle())
run_at_with_ssl[A, X](mut global_app, params)!
return
}
// Generate routes and controllers just like the original run() function.
routes := generate_routes[A, X](global_app)!
controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)!
// Allocate params on the heap to keep it valid for the server lifetime
request_params := &RequestParams{
global_app: unsafe { voidptr(&global_app) }
controllers_sorted: controllers_sorted
routes: &routes
benchmark_page_generation: params.benchmark_page_generation
}
// Configure and run the fasthttp server
mut server := fasthttp.new_server(fasthttp.ServerConfig{
family: params.family
port: params.port
handler: parallel_request_handler[A, X]
max_request_buffer_size: params.max_request_buffer_size
timeout_in_seconds: params.timeout_in_seconds
user_data: voidptr(request_params)
}) or {
eprintln('Failed to create server: ${err}')
return
}
handle := server.handle()
maybe_init_server[A](mut global_app, new_server_with_lifecycle(handle))
println('[veb] Running multi-threaded app on ${server_protocol(params)}://${startup_host(params)}:${params.port}/')
flush_stdout()
$if A is BeforeAcceptApp {
mut server_thread := spawn_fasthttp_server_run(mut server)
// Wait until the listener is bound before invoking before_accept_loop,
// so callers using `<-app.started` actually see the server ready.
handle.wait_till_running() or {}
global_app.before_accept_loop()
server_thread.wait() or { panic(err) }
} $else {
server.run() or { panic(err) }
}
}
fn spawn_fasthttp_server_run(mut server fasthttp.Server) thread ! {
return spawn server.run()
}
fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpResponse {
// Get parameters from user_data - copy to avoid use-after-free
params := unsafe { *(&RequestParams(req.user_data)) }
mut global_app := unsafe { &A(params.global_app) }
client_fd := req.client_conn_fd
head_end := if req.body.start > 0 { req.body.start } else { req.buffer.len }
head := req.buffer[..head_end].bytestr()
// Parse the request head into a standard `http.Request`, then copy just the body.
mut req2 := http.parse_request_head_str(head) or {
return fasthttp.HttpResponse{
content: 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
content_owned: true
}
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb parsed http head') }
}
if req.body.len > 0 {
req2.data = req.buffer[req.body.start..req.body.start + req.body.len].bytestr()
}
// If the request uses chunked transfer encoding, decode the chunked body
if transfer_encoding_is_chunked(req2.header) {
req2.data = decode_chunked_body(req2.data) or {
return fasthttp.HttpResponse{
content: 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
content_owned: true
}
}
}
if invalid_resp := content_length_validation_response(req, req2) {
return invalid_resp
}
// Create and populate the `veb.Context`.
mut completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, params)
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb handled route') }
}
match completed_context.takeover_mode {
.manual {
// The handler has taken over the connection (e.g. for SSE or WebSocket).
// The response was already sent directly over ctx.conn.
// Tell fasthttp to hand off the fd without closing it.
return fasthttp.HttpResponse{
takeover_mode: .manual
}
}
.reusable {
should_close := should_close_connection(completed_context.req, completed_context.res,
completed_context.client_wants_to_close)
return fasthttp.HttpResponse{
takeover_mode: .reusable
should_close: should_close
}
}
.none {}
}
should_close := should_close_connection(completed_context.req, completed_context.res,
completed_context.client_wants_to_close)
content := completed_context.res.bytes()
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb serialized response') }
}
unsafe { completed_context.res.body.free() }
completed_context.res.body = ''
return_type := completed_context.return_type
return_file := completed_context.return_file
unsafe { free(completed_context) }
if return_type == .file {
return fasthttp.HttpResponse{
content: content
content_owned: true
file_path: return_file
should_close: should_close
}
}
// The fasthttp server expects a complete response buffer to be returned.
return fasthttp.HttpResponse{
content: content
content_owned: true
should_close: should_close
}
} // handle_request_and_route is a unified function that creates the context,
fn content_length_validation_response(req fasthttp.HttpRequest, parsed http.Request) ?fasthttp.HttpResponse {
if transfer_encoding_is_chunked(parsed.header) {
return none
}
content_length := parsed.header.get(.content_length) or { return none }
expected_length := content_length.int()
actual_length := req.body.len
if actual_length == expected_length {
return none
}
if actual_length < expected_length {
return fasthttp.HttpResponse{
content: http_408.bytes()
content_owned: true
}
}
return fasthttp.HttpResponse{
content: 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)
).bytes()
content_owned: true
}
}
// runs middleware, and finds the correct route for a request.
fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, params RequestParams) &Context {
// Create and populate the `veb.Context` from the request.
mut url := urllib.parse_request_uri(req.url) or {
// This should be rare if http.parse_request succeeded.
mut bad_ctx := &Context{
req: req
}
bad_ctx.not_found()
return bad_ctx
}
query := parse_query_from_url(url)
form, files := parse_form_from_request(req) or {
mut bad_ctx := &Context{
req: req
}
bad_ctx.request_error('Failed to parse form data: ${err.msg()}')
return bad_ctx
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb parsed url/form') }
}
host_with_port := req.header.get(.host) or { '' }
host := request_host_name(host_with_port)
page_gen_start := if params.benchmark_page_generation { time.ticks() } else { 0 }
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb parsed host') }
}
mut ctx := &Context{
req: req
page_gen_start: page_gen_start
client_fd: _client_fd
client_wants_to_close: request_has_connection_close(req)
query: query
form: form
files: files
}
mut user_context := X{
Context: ctx
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb context initialized') }
}
$if A is StaticApp {
ctx.custom_mime_types_ref = unsafe { &app.static_mime_types }
if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types,
app.static_hosts, app.static_prefixes, app.enable_static_gzip, app.enable_static_zstd,
app.enable_static_compression, app.static_compression_max_size,
app.static_compression_mime_types, app.enable_markdown_negotiation), mut user_context,
url, host)
{
// Preserve the handled context on the heap before the stack-local user context goes away.
unsafe {
*ctx = user_context.Context
}
return ctx
}
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb pre-route static checked') }
}
// Match controller paths first
$if A is ControllerInterface {
if completed_context := handle_controllers[X](params.controllers_sorted, ctx, mut url, host) {
return completed_context
}
}
// Create a new user context and pass veb's context
handle_route[A, X](mut app, mut user_context, url, host, params.routes)
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'veb route returned') }
}
// Preserve the handled context on the heap before the stack-local user context goes away.
unsafe {
*ctx = user_context.Context
}
return ctx
}
// decode_chunked_body decodes a chunked transfer-encoded body string
// into the raw body content.
fn decode_chunked_body(data string) !string {
mut sb := strings.new_builder(data.len)
mut pos := 0
for pos < data.len {
// Find the end of the chunk size line
line_end := data.index_after_('\r\n', pos)
if line_end == -1 {
return error('invalid chunked body: missing chunk size line ending')
}
chunk_size_str := data[pos..line_end].all_before(';').trim_space()
if chunk_size_str.len == 0 {
return error('invalid chunked body: empty chunk size')
}
chunk_size := int(strconv.parse_uint(chunk_size_str, 16, 64) or {
return error('invalid chunked body: bad chunk size')
})
pos = line_end + 2 // skip past \r\n
if chunk_size == 0 {
// Terminal chunk - skip trailers
break
}
if pos + chunk_size > data.len {
return error('invalid chunked body: chunk data truncated')
}
sb.write_string(data[pos..pos + chunk_size])
pos += chunk_size + 2 // skip chunk data + \r\n delimiter
}
return sb.str()
}