v2 / vlib / veb / veb.v
1001 lines · 931 sloc · 30.38 KB · 45545c2fda3dfafa31fb7341b31b786ad143e67d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module veb
5
6import io
7import net
8import net.http
9import net.mbedtls
10import net.urllib
11import os
12import strconv
13import strings
14import time
15
16// A type which doesn't get filtered inside templates
17pub type RawHtml = string
18
19// A dummy structure that returns from routes to indicate that you actually sent something to a user
20@[noinit]
21pub struct Result {}
22
23// no_result does nothing, but returns `veb.Result`. Only use it when you are sure
24// a response will be send over the connection, or in combination with `Context.takeover_conn`
25pub fn no_result() Result {
26 return Result{}
27}
28
29struct Route {
30 methods []http.Method
31 path string
32 words []string
33 host string
34mut:
35 middlewares []RouteMiddleware
36 after_middlewares []RouteMiddleware
37}
38
39fn route_path_words(path string) []string {
40 if path.len == 0 || path == '/' {
41 return []string{}
42 }
43 return path.split('/').filter(it != '')
44}
45
46// Generate route structs for an app
47fn generate_routes[A, X](app &A) !map[string]Route {
48 // Parsing methods attributes
49 mut routes := map[string]Route{}
50 $for method in A.methods {
51 $if method.return_type is Result {
52 http_methods, route_path, host := parse_attrs(method.name, method.attrs) or {
53 return error('error parsing method attributes: ${err}')
54 }
55
56 mut route := Route{
57 methods: http_methods
58 path: route_path
59 words: route_path_words(route_path)
60 host: host
61 }
62
63 $if A is MiddlewareApp {
64 route.middlewares = app_route_handlers(app, route_path)
65 route.after_middlewares = app_route_handlers_after(app, route_path)
66 }
67
68 routes[method.name] = route
69 } $else {
70 // If we have route attributes, but the wrong return type, return an error
71 if has_route_attributes(method.attrs) {
72 return error('method `${method.name}` at `${method.location}` has route attributes but invalid return type. Handler methods must return `veb.Result`, not `!veb.Result` or other types')
73 }
74 }
75 }
76 return routes
77}
78
79// run - start a new veb server, listening to all available addresses, at the specified `port`
80pub fn run[A, X](mut global_app A, port int) {
81 run_at[A, X](mut global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
82}
83
84@[params]
85pub struct RunParams {
86pub:
87 // use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
88 family net.AddrFamily = .ip6
89 host string
90 port int = default_port
91 nr_workers int = 1
92 show_startup_message bool = true
93 timeout_in_seconds int = 30
94 max_request_buffer_size int = 8192
95 benchmark_page_generation bool // for the "page rendered in X ms"
96 ssl_config mbedtls.SSLConnectConfig
97}
98
99struct SslRequestParams {
100 global_app voidptr
101 controllers_sorted []&ControllerPath
102 routes &map[string]Route
103 benchmark_page_generation bool
104 max_request_buffer_size int
105}
106
107fn ssl_enabled(params RunParams) bool {
108 return params.ssl_config.cert != '' || params.ssl_config.cert_key != ''
109}
110
111fn server_protocol(params RunParams) string {
112 if ssl_enabled(params) {
113 return 'https'
114 }
115 return 'http'
116}
117
118fn startup_host(params RunParams) string {
119 if params.host == '' {
120 return 'localhost'
121 }
122 return params.host
123}
124
125fn listen_addr(params RunParams) string {
126 if params.host == '' {
127 return ':${params.port}'
128 }
129 return '${params.host}:${params.port}'
130}
131
132fn run_at_with_ssl[A, X](mut global_app A, params RunParams) ! {
133 routes := generate_routes[A, X](global_app)!
134 controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)!
135 if params.show_startup_message {
136 println('[veb] Running app on https://${startup_host(params)}:${params.port}/')
137 }
138 flush_stdout()
139 mut ssl_listener := mbedtls.new_ssl_listener(listen_addr(params), params.ssl_config)!
140 defer {
141 ssl_listener.shutdown() or {}
142 }
143 ssl_params := &SslRequestParams{
144 global_app: unsafe { voidptr(&global_app) }
145 controllers_sorted: controllers_sorted
146 routes: &routes
147 benchmark_page_generation: params.benchmark_page_generation
148 max_request_buffer_size: if params.max_request_buffer_size > 0 {
149 params.max_request_buffer_size
150 } else {
151 max_read
152 }
153 }
154 $if A is BeforeAcceptApp {
155 global_app.before_accept_loop()
156 }
157 for {
158 mut ssl_conn := ssl_listener.accept() or {
159 eprintln('[veb] accept() failed, reason: ${err}; skipping')
160 continue
161 }
162 ssl_conn.duration = params.timeout_in_seconds * time.second
163 spawn handle_ssl_connection[A, X](mut ssl_conn, ssl_params)
164 }
165}
166
167fn handle_ssl_connection[A, X](mut ssl_conn mbedtls.SSLConn, params &SslRequestParams) {
168 defer {
169 ssl_conn.shutdown() or {}
170 }
171 mut reader := io.new_buffered_reader(
172 reader: ssl_conn
173 cap: params.max_request_buffer_size
174 )
175 defer {
176 unsafe {
177 reader.free()
178 }
179 }
180 for {
181 req := read_request_from_buffered_reader(mut reader) or {
182 if err !is io.Eof {
183 write_ssl_response(mut ssl_conn, http_400) or {}
184 }
185 return
186 }
187 completed_context := handle_ssl_request[A, X](req, params) or {
188 write_ssl_response(mut ssl_conn, http_400) or {}
189 return
190 }
191 if completed_context.takeover_mode != .none {
192 eprintln('[veb] HTTPS connections do not support takeover connections yet; closing the connection after this response.')
193 }
194 write_ssl_context_response(mut ssl_conn, completed_context) or {
195 eprintln('[veb] error sending HTTPS response: ${err}')
196 return
197 }
198 if completed_context.takeover_mode != .none
199 || should_close_connection(completed_context.req, completed_context.res, completed_context.client_wants_to_close) {
200 return
201 }
202 }
203}
204
205fn read_request_from_buffered_reader(mut reader io.BufferedReader) !http.Request {
206 mut req := http.parse_request_head(mut reader)!
207 if transfer_encoding_is_chunked(req.header) {
208 req.data = read_chunked_request_body(mut reader)!
209 return req
210 }
211 content_length := req.header.get(.content_length) or { '0' }
212 content_length_i := content_length.int()
213 if content_length_i <= 0 {
214 return req
215 }
216 mut body := []u8{len: content_length_i}
217 read_exact_bytes(mut reader, mut body)!
218 req.data = body.bytestr()
219 return req
220}
221
222fn transfer_encoding_is_chunked(header http.Header) bool {
223 transfer_encoding := header.get(.transfer_encoding) or { return false }
224 for word in transfer_encoding.to_lower().split(',') {
225 if word.trim_space() == 'chunked' {
226 return true
227 }
228 }
229 return false
230}
231
232fn read_chunked_request_body(mut reader io.BufferedReader) !string {
233 mut sb := strings.new_builder(1024)
234 for {
235 mut chunk_size_line := reader.read_line()!
236 if semicolon_idx := chunk_size_line.index(';') {
237 chunk_size_line = chunk_size_line[..semicolon_idx]
238 }
239 chunk_size_line = chunk_size_line.trim_space()
240 if chunk_size_line.len == 0 {
241 return error('invalid chunk size line')
242 }
243 chunk_size_u64 := strconv.parse_uint(chunk_size_line, 16, 64) or {
244 return error('invalid chunk size line')
245 }
246 if chunk_size_u64 > u64(max_int) {
247 return error('chunk size too large')
248 }
249 chunk_size := int(chunk_size_u64)
250 if chunk_size == 0 {
251 for {
252 trailer := reader.read_line()!
253 if trailer == '' {
254 return sb.str()
255 }
256 }
257 }
258 mut chunk := []u8{len: chunk_size}
259 read_exact_bytes(mut reader, mut chunk)!
260 sb.write(chunk)!
261 mut delimiter := []u8{len: 2}
262 read_exact_bytes(mut reader, mut delimiter)!
263 if delimiter[0] != `\r` || delimiter[1] != `\n` {
264 return error('invalid chunk delimiter')
265 }
266 }
267 return error('invalid chunked body')
268}
269
270fn read_exact_bytes(mut reader io.BufferedReader, mut buf []u8) ! {
271 mut offset := 0
272 for offset < buf.len {
273 offset += reader.read(mut buf[offset..])!
274 }
275}
276
277fn handle_ssl_request[A, X](req http.Request, params &SslRequestParams) ?&Context {
278 mut global_app := unsafe { &A(params.global_app) }
279 page_gen_start := time.ticks()
280 mut url := urllib.parse(req.url) or {
281 eprintln('[veb] error parsing path "${req.url}": ${err}')
282 return none
283 }
284 query := parse_query_from_url(url)
285 form, files := parse_form_from_request(req) or {
286 eprintln('[veb] error parsing form: ${err.msg()}')
287 return none
288 }
289 host_with_port := req.header.get(.host) or { '' }
290 host := request_host_name(host_with_port)
291 mut ctx := &Context{
292 req: req
293 page_gen_start: page_gen_start
294 query: query
295 form: form
296 files: files
297 }
298 ctx.client_wants_to_close = request_has_connection_close(req)
299 mut user_context := X{
300 Context: ctx
301 }
302 $if A is StaticApp {
303 ctx.custom_mime_types_ref = unsafe { &global_app.static_mime_types }
304 if serve_if_static[X](static_handler_config(global_app.static_files,
305 global_app.static_mime_types, global_app.static_hosts, global_app.static_prefixes,
306 global_app.enable_static_gzip, global_app.enable_static_zstd,
307 global_app.enable_static_compression, global_app.static_compression_max_size,
308 global_app.static_compression_mime_types, global_app.enable_markdown_negotiation), mut
309 user_context, url, host)
310 {
311 // Preserve the handled context on the heap before the stack-local user context goes away.
312 unsafe {
313 *ctx = user_context.Context
314 }
315 return ctx
316 }
317 }
318 $if A is ControllerInterface {
319 if completed_context := handle_controllers[X](params.controllers_sorted, ctx, mut url, host) {
320 return completed_context
321 }
322 }
323 handle_route[A, X](mut global_app, mut user_context, url, host, params.routes)
324 // Preserve the handled context on the heap before the stack-local user context goes away.
325 unsafe {
326 *ctx = user_context.Context
327 }
328 return ctx
329}
330
331fn write_ssl_context_response(mut ssl_conn mbedtls.SSLConn, completed_context &Context) ! {
332 if !completed_context.done && completed_context.return_type == .normal {
333 return error('context did not send a response')
334 }
335 match completed_context.return_type {
336 .normal {
337 write_ssl_response(mut ssl_conn, completed_context.res)!
338 }
339 .file {
340 write_ssl_response(mut ssl_conn, completed_context.res)!
341 if completed_context.return_file == '' {
342 return error('missing file response path')
343 }
344 mut file := os.open(completed_context.return_file)!
345 defer {
346 file.close()
347 }
348 mut buf := []u8{len: max_read}
349 for {
350 n := file.read(mut buf) or {
351 if err is io.Eof {
352 break
353 }
354 return err
355 }
356 if n <= 0 {
357 break
358 }
359 ssl_conn.write(buf[..n])!
360 }
361 }
362 }
363}
364
365fn write_ssl_response(mut ssl_conn mbedtls.SSLConn, resp http.Response) ! {
366 ssl_conn.write(resp.bytes())!
367}
368
369fn request_has_connection_close(req http.Request) bool {
370 conn := req.header.get(.connection) or { return false }
371 return ascii_eq_ignore_case(conn, 'close')
372}
373
374fn request_host_name(host_with_port string) string {
375 if host_with_port.len == 0 {
376 return ''
377 }
378 if host_with_port[0] == `[` {
379 end := host_with_port.index_u8(`]`)
380 if end > 0 {
381 return host_with_port[1..end]
382 }
383 return host_with_port
384 }
385 colon := host_with_port.last_index_u8(`:`)
386 if colon == -1 {
387 return host_with_port
388 }
389 for i in 0 .. colon {
390 if host_with_port[i] == `:` {
391 return host_with_port
392 }
393 }
394 return host_with_port[..colon]
395}
396
397fn ascii_eq_ignore_case(a string, b string) bool {
398 if a.len != b.len {
399 return false
400 }
401 for i in 0 .. a.len {
402 mut ca := a[i]
403 if ca >= `A` && ca <= `Z` {
404 ca += 32
405 }
406 mut cb := b[i]
407 if cb >= `A` && cb <= `Z` {
408 cb += 32
409 }
410 if ca != cb {
411 return false
412 }
413 }
414 return true
415}
416
417fn should_close_connection(req http.Request, resp http.Response, client_wants_to_close bool) bool {
418 if client_wants_to_close {
419 return true
420 }
421 if resp_conn := resp.header.get(.connection) {
422 if ascii_eq_ignore_case(resp_conn, 'close') {
423 return true
424 }
425 if ascii_eq_ignore_case(resp_conn, 'keep-alive') {
426 return false
427 }
428 }
429 if req_conn := req.header.get(.connection) {
430 if ascii_eq_ignore_case(req_conn, 'close') {
431 return true
432 }
433 if ascii_eq_ignore_case(req_conn, 'keep-alive') {
434 return false
435 }
436 }
437 return req.version != .v1_1
438}
439
440interface BeforeAcceptApp {
441mut:
442 before_accept_loop()
443}
444
445interface HasBeforeRequestOnContext {
446mut:
447 before_request()
448}
449
450fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string, routes &map[string]Route) {
451 mut route := Route{}
452 mut middleware_has_sent_response := false
453 mut not_found := false
454 mut variadic_route := Route{}
455 mut variadic_route_words := []string{}
456 mut variadic_method_name := ''
457 mut variadic_method_args := []string{}
458
459 defer {
460 // execute middleware functions after veb is done and before the response is send
461 mut was_done := true
462 $if A is MiddlewareApp {
463 if !not_found && !middleware_has_sent_response {
464 // if the middleware doesn't send an alternate response, but only changes the
465 // response object we only have to check if the `done` was previously set to true
466 was_done = user_context.Context.done
467 // reset `done` so the middleware functions can return a different response
468 // 1 time only, since the `done` guard is still present in
469 // `Context.send_response_to_client`
470 user_context.Context.done = false
471
472 // no need to check the result of `validate_middleware`, since a response has to be sent
473 // anyhow. This function makes sure no further middleware is executed.
474 validate_middleware[X](mut user_context, app_global_handlers_after(app))
475 // skip route-specific after-middleware if global already sent a response
476 if !user_context.Context.done {
477 validate_middleware[X](mut user_context, get_handlers_for_method(route.after_middlewares,
478 user_context.Context.req.method))
479 }
480 }
481 }
482 // send only the headers, because if the response body is too big, TcpConn code will
483 // actually block, because it has to wait for the socket to become ready to write. veb
484 // will handle this case.
485 if !was_done && !user_context.Context.done && user_context.Context.takeover_mode == .none {
486 eprintln('[veb] handler for route "${url.path}" does not send any data!')
487 // send response anyway so the connection won't block
488 // fast_send_resp_header(mut user_context.conn, user_context.res) or {}
489 } else if user_context.Context.takeover_mode == .none {
490 // fast_send_resp_header(mut user_context.conn, user_context.res) or {}
491 }
492 // Context.takeover_mode is set, so the user must send a response.
493 }
494
495 is_root_path := url.path.len == 0 || url.path == '/'
496
497 $if veb_livereload ? {
498 if url.path.starts_with('/veb_livereload/') {
499 if url.path.ends_with('current') {
500 user_context.handle_veb_livereload_current()
501 return
502 }
503 if url.path.ends_with('script.js') {
504 user_context.handle_veb_livereload_script()
505 return
506 }
507 }
508 }
509
510 // first execute before_request
511 $if X is HasBeforeRequestOnContext {
512 user_context.before_request()
513 }
514 // user_context.before_request()
515 if user_context.Context.done {
516 return
517 }
518 $if trace_prealloc ? {
519 unsafe { prealloc_scope_checkpoint(c'veb before_request done') }
520 }
521
522 // then execute global middleware functions
523 $if A is MiddlewareApp {
524 if validate_middleware[X](mut user_context, app_global_handlers(app)) == false {
525 middleware_has_sent_response = true
526 return
527 }
528 }
529
530 $if A is StaticApp {
531 should_check_static := !is_root_path || app.enable_markdown_negotiation
532 || app.static_files['/'] != '' || app.static_files['/index.html'] != ''
533 || app.static_files['/index.htm'] != ''
534 if should_check_static {
535 if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types,
536 app.static_hosts, app.static_prefixes, app.enable_static_gzip,
537 app.enable_static_zstd, app.enable_static_compression,
538 app.static_compression_max_size, app.static_compression_mime_types,
539 app.enable_markdown_negotiation), mut user_context, url, host)
540 {
541 // successfully served a static file
542 return
543 }
544 }
545 }
546 $if trace_prealloc ? {
547 unsafe { prealloc_scope_checkpoint(c'veb route static checked') }
548 }
549 $if trace_prealloc ? {
550 unsafe { prealloc_scope_checkpoint(c'veb before route match') }
551 }
552
553 if is_root_path {
554 $for method in A.methods {
555 $if method.name == 'index' && method.return_type is Result {
556 route = (*routes)[method.name] or {
557 eprintln('[veb] parsed attributes for the `${method.name}` are not found, skipping...')
558 Route{}
559 }
560 if user_context.Context.req.method in route.methods
561 && (route.host == '' || route.host == host)
562 && (route.path == '/' || route.path == '/index') {
563 $if A is MiddlewareApp {
564 if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares,
565 user_context.Context.req.method)) == false {
566 middleware_has_sent_response = true
567 return
568 }
569 }
570 can_have_data_args := user_context.Context.req.method == .post
571 || user_context.Context.req.method == .get
572 if method.args.len > 1 && can_have_data_args {
573 mut args := []string{cap: method.args.len + 1}
574 data := if user_context.Context.req.method == .get {
575 user_context.Context.query
576 } else {
577 user_context.Context.form
578 }
579 for param in method.args[1..] {
580 args << data[param.name]
581 }
582 $if trace_prealloc ? {
583 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
584 }
585 app.$method(mut user_context, ...args)
586 $if trace_prealloc ? {
587 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
588 }
589 } else {
590 $if trace_prealloc ? {
591 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
592 }
593 app.$method(mut user_context)
594 $if trace_prealloc ? {
595 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
596 }
597 }
598 return
599 }
600 }
601 }
602 }
603
604 url_words := route_path_words(url.path)
605 $if trace_prealloc ? {
606 unsafe { prealloc_scope_checkpoint(c'veb route words parsed') }
607 }
608
609 // Route matching and match route specific middleware as last step
610 $for method in A.methods {
611 $if method.return_type is Result {
612 route = (*routes)[method.name] or {
613 eprintln('[veb] parsed attributes for the `${method.name}` are not found, skipping...')
614 Route{}
615 }
616
617 // Skip if the HTTP request method does not match the attributes
618 if user_context.Context.req.method in route.methods {
619 // Used for route matching
620 route_words := route.words
621
622 // Skip if the host does not match or is empty
623 if route.host == '' || route.host == host {
624 can_have_data_args := user_context.Context.req.method == .post
625 || user_context.Context.req.method == .get
626 // Route immediate matches first
627 // For example URL `/register` matches route `/:user`, but `fn register()`
628 // should be called first.
629 if !route.path.contains('/:') && url_words == route_words {
630 // We found a match
631 $if A is MiddlewareApp {
632 if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares,
633 user_context.Context.req.method)) == false {
634 middleware_has_sent_response = true
635 return
636 }
637 }
638 if method.args.len > 1 && can_have_data_args {
639 // Populate method args with form or query values
640 mut args := []string{cap: method.args.len + 1}
641 data := if user_context.Context.req.method == .get {
642 user_context.Context.query
643 } else {
644 user_context.Context.form
645 }
646 for param in method.args[1..] {
647 args << data[param.name]
648 }
649 $if trace_prealloc ? {
650 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
651 }
652 app.$method(mut user_context, ...args)
653 $if trace_prealloc ? {
654 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
655 }
656 } else {
657 $if trace_prealloc ? {
658 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
659 }
660 app.$method(mut user_context)
661 $if trace_prealloc ? {
662 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
663 }
664 }
665 return
666 }
667
668 if url_words.len == 0 && route_words.len == 1 && route_words[0] == 'index'
669 && method.name == 'index' {
670 $if A is MiddlewareApp {
671 if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares,
672 user_context.Context.req.method)) == false {
673 middleware_has_sent_response = true
674 return
675 }
676 }
677
678 if method.args.len > 1 && can_have_data_args {
679 // Populate method args with form or query values
680 mut args := []string{cap: method.args.len + 1}
681 data := if user_context.Context.req.method == .get {
682 user_context.Context.query
683 } else {
684 user_context.Context.form
685 }
686 for param in method.args[1..] {
687 args << data[param.name]
688 }
689 $if trace_prealloc ? {
690 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
691 }
692 app.$method(mut user_context, ...args)
693 $if trace_prealloc ? {
694 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
695 }
696 } else {
697 $if trace_prealloc ? {
698 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
699 }
700 app.$method(mut user_context)
701 $if trace_prealloc ? {
702 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
703 }
704 }
705 return
706 }
707
708 if params := route_matches(url_words, route_words) {
709 if route_is_variadic(route_words) {
710 if should_prefer_variadic_route(route_words, variadic_route_words) {
711 variadic_route = route
712 variadic_route_words = route_words.clone()
713 variadic_method_name = method.name
714 variadic_method_args = params.clone()
715 }
716 } else {
717 $if A is MiddlewareApp {
718 if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares,
719 user_context.Context.req.method)) == false {
720 middleware_has_sent_response = true
721 return
722 }
723 }
724 method_args := params.clone()
725 if method_args.len + 1 != method.args.len {
726 eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})')
727 }
728 $if trace_prealloc ? {
729 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
730 }
731 app.$method(mut user_context, ...method_args)
732 $if trace_prealloc ? {
733 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
734 }
735 return
736 }
737 }
738 }
739 }
740 }
741 }
742 if variadic_method_name != '' {
743 route = variadic_route
744 $for method in A.methods {
745 $if method.return_type is Result {
746 if method.name == variadic_method_name {
747 $if A is MiddlewareApp {
748 if validate_middleware[X](mut user_context, get_handlers_for_method(variadic_route.middlewares,
749 user_context.Context.req.method)) == false {
750 middleware_has_sent_response = true
751 return
752 }
753 }
754 method_args := variadic_method_args.clone()
755 if method_args.len + 1 != method.args.len {
756 eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})')
757 }
758 $if trace_prealloc ? {
759 unsafe { prealloc_scope_checkpoint(c'veb before route handler') }
760 }
761 app.$method(mut user_context, ...method_args)
762 $if trace_prealloc ? {
763 unsafe { prealloc_scope_checkpoint(c'veb after route handler') }
764 }
765 return
766 }
767 }
768 }
769 }
770 // return 404
771 user_context.not_found()
772 not_found = true
773 return
774}
775
776fn route_is_variadic(route_words []string) bool {
777 return route_words.len > 0 && route_words[route_words.len - 1].ends_with('...')
778}
779
780fn should_prefer_variadic_route(candidate []string, current []string) bool {
781 if current.len == 0 {
782 return true
783 }
784 if candidate.len != current.len {
785 return candidate.len > current.len
786 }
787 return variadic_route_static_parts(candidate) > variadic_route_static_parts(current)
788}
789
790fn variadic_route_static_parts(route_words []string) int {
791 mut static_parts := 0
792 for route_word in route_words[..route_words.len - 1] {
793 if !route_word.starts_with(':') {
794 static_parts++
795 }
796 }
797 return static_parts
798}
799
800fn route_matches(url_words []string, route_words []string) ?[]string {
801 // URL path should be at least as long as the route path
802 // except for the catchall route (`/:path...`)
803 if route_words.len == 1 && route_words[0].starts_with(':') && route_words[0].ends_with('...') {
804 return ['/' + url_words.join('/')]
805 }
806 if url_words.len < route_words.len {
807 return none
808 }
809 mut params := []string{cap: url_words.len}
810 if url_words.len == route_words.len {
811 for i in 0 .. url_words.len {
812 if route_words[i].starts_with(':') {
813 // We found a path parameter
814 params << url_words[i]
815 } else if route_words[i] != url_words[i] {
816 // This url does not match the route
817 return none
818 }
819 }
820 return params
821 }
822
823 // The last route can end with ... indicating an array
824 if route_words.len == 0 || !route_words[route_words.len - 1].ends_with('...') {
825 return none
826 }
827
828 for i in 0 .. route_words.len - 1 {
829 if route_words[i].starts_with(':') {
830 // We found a path parameter
831 params << url_words[i]
832 } else if route_words[i] != url_words[i] {
833 // This url does not match the route
834 return none
835 }
836 }
837 params << url_words[route_words.len - 1..url_words.len].join('/')
838 return params
839}
840
841// check if request is for a static file and serves it
842// returns true if we served a static file, false otherwise
843fn serve_if_static[X](app StaticHandler, mut user_context X, url urllib.URL, host string) bool {
844 // TODO: handle url parameters properly - for now, ignore them
845 mut asked_path := url.path
846 static_handler := app
847 if !static_handler.enable_markdown_negotiation && asked_path == '/'
848 && static_handler.static_files['/'] == ''
849 && static_handler.static_files['/index.html'] == ''
850 && static_handler.static_files['/index.htm'] == '' {
851 return false
852 }
853 if !static_handler.enable_markdown_negotiation
854 && !static_handler.may_contain_static_path(asked_path) {
855 return false
856 }
857
858 // Content negotiation for markdown files (if enabled)
859 if static_handler.enable_markdown_negotiation {
860 accept_header := user_context.req.header.get(.accept) or { '' }
861 if accept_header.contains('text/markdown') {
862 // Try markdown variants in order of priority
863 markdown_variants := [
864 asked_path + '.md',
865 asked_path + '.html.md',
866 asked_path + '/index.html.md',
867 ]
868
869 for variant in markdown_variants {
870 if static_handler.static_files[variant] != '' {
871 asked_path = variant
872 break
873 }
874 }
875 }
876 }
877
878 base_path := os.base(asked_path)
879 if !base_path.contains('.') && !asked_path.ends_with('/') {
880 asked_path += '/'
881 }
882
883 if asked_path.ends_with('/') {
884 // Check for markdown index first if Accept header requests it and feature is enabled
885 if static_handler.enable_markdown_negotiation {
886 accept_header := user_context.req.header.get(.accept) or { '' }
887 if accept_header.contains('text/markdown')
888 && static_handler.static_files[asked_path + 'index.html.md'] != '' {
889 asked_path += 'index.html.md'
890 } else if static_handler.static_files[asked_path + 'index.html'] != '' {
891 asked_path += 'index.html'
892 } else if static_handler.static_files[asked_path + 'index.htm'] != '' {
893 asked_path += 'index.htm'
894 }
895 } else if static_handler.static_files[asked_path + 'index.html'] != '' {
896 asked_path += 'index.html'
897 } else if static_handler.static_files[asked_path + 'index.htm'] != '' {
898 asked_path += 'index.htm'
899 }
900 }
901 static_file := static_handler.static_files[asked_path] or { return false }
902
903 // StaticHandler ensures that the mime type exists on either the App or in veb
904 ext := os.file_ext(static_file).to_lower()
905 mut mime_type := static_handler.static_mime_types[ext] or { mime_types[ext] }
906
907 static_host := static_handler.static_hosts[asked_path] or { '' }
908 if static_file == '' || mime_type == '' {
909 return false
910 }
911 if static_host != '' && static_host != host {
912 return false
913 }
914
915 // Configure static file compression settings
916 user_context.set_static_compression_config(static_handler.enable_static_gzip,
917 static_handler.enable_static_zstd, static_handler.enable_static_compression, if static_handler.static_compression_max_size >= 0 {
918 static_handler.static_compression_max_size
919 } else {
920 1048576 // Default: 1MB
921 }, static_handler.static_compression_mime_types)
922
923 user_context.send_file(mime_type, static_file)
924 return true
925}
926
927fn (sh StaticHandler) may_contain_static_path(path string) bool {
928 if sh.static_prefixes.len == 0 || path == '/' {
929 return true
930 }
931 for prefix in sh.static_prefixes {
932 if prefix.ends_with('/') {
933 if path.starts_with(prefix) {
934 return true
935 }
936 } else if path == prefix {
937 return true
938 }
939 }
940 return false
941}
942
943fn static_handler_config(static_files map[string]string, static_mime_types map[string]string, static_hosts map[string]string, static_prefixes []string, enable_static_gzip bool, enable_static_zstd bool, enable_static_compression bool, static_compression_max_size int, static_compression_mime_types []string, enable_markdown_negotiation bool) StaticHandler {
944 return StaticHandler{
945 static_files: static_files
946 static_mime_types: static_mime_types
947 static_hosts: static_hosts
948 static_prefixes: static_prefixes
949 enable_static_gzip: enable_static_gzip
950 enable_static_zstd: enable_static_zstd
951 enable_static_compression: enable_static_compression
952 static_compression_max_size: static_compression_max_size
953 static_compression_mime_types: static_compression_mime_types
954 enable_markdown_negotiation: enable_markdown_negotiation
955 }
956}
957
958// send a string over `conn`
959fn send_string(mut conn net.TcpConn, s string) ! {
960 $if trace_send_string_conn ? {
961 eprintln('> send_string: conn: ${ptr_str(conn)}')
962 }
963 $if trace_response ? {
964 eprintln('> send_string:\n${s}\n')
965 }
966 if voidptr(conn) == unsafe { nil } {
967 return error('connection was closed before send_string')
968 }
969 conn.write_string(s)!
970}
971
972// Set s to the form error
973pub fn (mut ctx Context) error(s string) {
974 eprintln('[veb] Context.error: ${s}')
975 ctx.form_error = s
976}
977
978fn fast_send_resp_header(mut conn net.TcpConn, resp http.Response) ! {
979 mut sb := strings.new_builder(resp.body.len + 200)
980 sb.write_string('HTTP/')
981 sb.write_string(resp.http_version)
982 sb.write_string(' ')
983 sb.write_decimal(resp.status_code)
984 sb.write_string(' ')
985 sb.write_string(resp.status_msg)
986 sb.write_string('\r\n')
987
988 resp.header.render_into_sb(mut sb,
989 version: resp.version()
990 )
991 sb.write_string('\r\n')
992 send_string(mut conn, sb.str())!
993}
994
995// Formats resp to a string suitable for HTTP response transmission
996// A fast version of `resp.bytestr()` used with
997// `send_string(mut ctx.conn, resp.bytestr())`
998fn fast_send_resp(mut conn net.TcpConn, resp http.Response) ! {
999 fast_send_resp_header(mut conn, resp)!
1000 send_string(mut conn, resp.body)!
1001}
1002