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