v

Зеркало из https://github.com/vlang/v
Форк
0
/
vweb.v 
1243 строки · 35.9 Кб
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.
4
//@[deprecated: '`vweb` is deprecated `veb`. Please use `veb` instead.']
5
module vweb
6

7
import os
8
import io
9
import runtime
10
import net
11
import net.http
12
import net.urllib
13
import time
14
import json
15
import encoding.html
16
import context
17
import strings
18

19
// A type which don't get filtered inside templates
20
pub type RawHtml = string
21

22
// A dummy structure that returns from routes to indicate that you actually sent something to a user
23
@[noinit]
24
pub struct Result {}
25

26
pub const methods_with_form = [http.Method.post, .put, .patch]
27
pub const headers_close = http.new_custom_header_from_map({
28
	'Server':                           'VWeb'
29
	http.CommonHeader.connection.str(): 'close'
30
}) or { panic('should never fail') }
31

32
pub const http_302 = http.new_response(
33
	status: .found
34
	body:   '302 Found'
35
	header: headers_close
36
)
37
pub const http_303 = http.new_response(
38
	status: .see_other
39
	body:   '303 See Other'
40
	header: headers_close
41
)
42
pub const http_400 = http.new_response(
43
	status: .bad_request
44
	body:   '400 Bad Request'
45
	header: http.new_header(
46
		key:   .content_type
47
		value: 'text/plain'
48
	).join(headers_close)
49
)
50
pub const http_404 = http.new_response(
51
	status: .not_found
52
	body:   '404 Not Found'
53
	header: http.new_header(
54
		key:   .content_type
55
		value: 'text/plain'
56
	).join(headers_close)
57
)
58
pub const http_500 = http.new_response(
59
	status: .internal_server_error
60
	body:   '500 Internal Server Error'
61
	header: http.new_header(
62
		key:   .content_type
63
		value: 'text/plain'
64
	).join(headers_close)
65
)
66
pub const mime_types = {
67
	'.aac':    'audio/aac'
68
	'.abw':    'application/x-abiword'
69
	'.arc':    'application/x-freearc'
70
	'.avi':    'video/x-msvideo'
71
	'.azw':    'application/vnd.amazon.ebook'
72
	'.bin':    'application/octet-stream'
73
	'.bmp':    'image/bmp'
74
	'.bz':     'application/x-bzip'
75
	'.bz2':    'application/x-bzip2'
76
	'.cda':    'application/x-cdf'
77
	'.csh':    'application/x-csh'
78
	'.css':    'text/css'
79
	'.csv':    'text/csv'
80
	'.doc':    'application/msword'
81
	'.docx':   'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
82
	'.eot':    'application/vnd.ms-fontobject'
83
	'.epub':   'application/epub+zip'
84
	'.gz':     'application/gzip'
85
	'.gif':    'image/gif'
86
	'.htm':    'text/html'
87
	'.html':   'text/html'
88
	'.ico':    'image/vnd.microsoft.icon'
89
	'.ics':    'text/calendar'
90
	'.jar':    'application/java-archive'
91
	'.jpeg':   'image/jpeg'
92
	'.jpg':    'image/jpeg'
93
	'.js':     'text/javascript'
94
	'.json':   'application/json'
95
	'.jsonld': 'application/ld+json'
96
	'.md':     'text/markdown'
97
	'.mid':    'audio/midi audio/x-midi'
98
	'.midi':   'audio/midi audio/x-midi'
99
	'.mjs':    'text/javascript'
100
	'.mp3':    'audio/mpeg'
101
	'.mp4':    'video/mp4'
102
	'.mpeg':   'video/mpeg'
103
	'.mpkg':   'application/vnd.apple.installer+xml'
104
	'.odp':    'application/vnd.oasis.opendocument.presentation'
105
	'.ods':    'application/vnd.oasis.opendocument.spreadsheet'
106
	'.odt':    'application/vnd.oasis.opendocument.text'
107
	'.oga':    'audio/ogg'
108
	'.ogv':    'video/ogg'
109
	'.ogx':    'application/ogg'
110
	'.opus':   'audio/opus'
111
	'.otf':    'font/otf'
112
	'.png':    'image/png'
113
	'.pdf':    'application/pdf'
114
	'.php':    'application/x-httpd-php'
115
	'.ppt':    'application/vnd.ms-powerpoint'
116
	'.pptx':   'application/vnd.openxmlformats-officedocument.presentationml.presentation'
117
	'.rar':    'application/vnd.rar'
118
	'.rtf':    'application/rtf'
119
	'.sh':     'application/x-sh'
120
	'.svg':    'image/svg+xml'
121
	'.swf':    'application/x-shockwave-flash'
122
	'.tar':    'application/x-tar'
123
	'.toml':   'application/toml'
124
	'.tif':    'image/tiff'
125
	'.tiff':   'image/tiff'
126
	'.ts':     'video/mp2t'
127
	'.ttf':    'font/ttf'
128
	'.txt':    'text/plain'
129
	'.vsd':    'application/vnd.visio'
130
	'.wasm':   'application/wasm'
131
	'.wav':    'audio/wav'
132
	'.weba':   'audio/webm'
133
	'.webm':   'video/webm'
134
	'.webp':   'image/webp'
135
	'.woff':   'font/woff'
136
	'.woff2':  'font/woff2'
137
	'.xhtml':  'application/xhtml+xml'
138
	'.xls':    'application/vnd.ms-excel'
139
	'.xlsx':   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
140
	'.xml':    'application/xml'
141
	'.xul':    'application/vnd.mozilla.xul+xml'
142
	'.zip':    'application/zip'
143
	'.3gp':    'video/3gpp'
144
	'.3g2':    'video/3gpp2'
145
	'.7z':     'application/x-7z-compressed'
146
	'.m3u8':   'application/vnd.apple.mpegurl'
147
	'.vsh':    'text/x-vlang'
148
	'.v':      'text/x-vlang'
149
}
150
pub const max_http_post_size = 1024 * 1024
151
pub const default_port = 8080
152

153
// The Context struct represents the Context which hold the HTTP request and response.
154
// It has fields for the query, form, files.
155
pub struct Context {
156
mut:
157
	content_type string          = 'text/plain'
158
	status       string          = '200 OK'
159
	ctx          context.Context = context.EmptyContext{}
160
pub mut:
161
	// HTTP Request
162
	req http.Request
163
	// TODO: Response
164
	done bool
165
	// time.ticks() from start of vweb connection handle.
166
	// You can use it to determine how much time is spent on your request.
167
	page_gen_start i64
168
	// TCP connection to client.
169
	// But beware, do not store it for further use, after request processing vweb will close connection.
170
	conn              &net.TcpConn = unsafe { nil }
171
	static_files      map[string]string
172
	static_mime_types map[string]string
173
	static_hosts      map[string]string
174
	// Map containing query params for the route.
175
	// http://localhost:3000/index?q=vpm&order_by=desc => { 'q': 'vpm', 'order_by': 'desc' }
176
	query map[string]string
177
	// Multipart-form fields.
178
	form map[string]string
179
	// Files from multipart-form.
180
	files map[string][]http.FileData
181

182
	header http.Header // response headers
183
	// ? It doesn't seem to be used anywhere
184
	form_error                  string
185
	livereload_poll_interval_ms int = 250
186
}
187

188
struct FileData {
189
pub:
190
	filename     string
191
	content_type string
192
	data         string
193
}
194

195
struct Route {
196
	methods    []http.Method
197
	path       string
198
	path_words []string // precalculated once to avoid split() allocations in handle_conn()
199
	middleware string
200
	host       string
201
}
202

203
// Defining this method is optional.
204
// This method called at server start.
205
// You can use it for initializing globals.
206
pub fn (ctx Context) init_server() {
207
	eprintln('init_server() has been deprecated, please init your web app in `fn main()`')
208
}
209

210
// before_accept_loop is called once the vweb app is started, and listening, but before the loop that accepts
211
// incoming request connections.
212
// It will be called in the main thread, that runs vweb.run/2 or vweb.run_at/2.
213
// It allows you to be notified about the successful start of your app, and to synchronise your other threads
214
// with the webserver start, without error prone and slow pooling or time.sleep waiting.
215
// Defining this method is optional.
216
pub fn (ctx &Context) before_accept_loop() {
217
}
218

219
// before_request is called once before each request is routed.
220
// It will be called in one of multiple threads in a pool, serving requests,
221
// the same one, in which the matching route method will be executed right after it.
222
// Defining this method is optional.
223
pub fn (ctx Context) before_request() {}
224

225
// TODO: test
226
// vweb intern function
227
@[manualfree]
228
pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool {
229
	if ctx.done {
230
		return false
231
	}
232
	ctx.done = true
233

234
	mut resp := http.Response{
235
		body: res
236
	}
237
	$if vweb_livereload ? {
238
		if mimetype == 'text/html' {
239
			resp.body = res.replace('</html>', '<script src="/vweb_livereload/${vweb_livereload_server_start}/script.js"></script>\n</html>')
240
		}
241
	}
242
	// build the header after the potential modification of resp.body from above
243
	header := http.new_header_from_map({
244
		.content_type:   mimetype
245
		.content_length: resp.body.len.str()
246
	}).join(ctx.header)
247
	resp.header = header.join(headers_close)
248

249
	resp.set_version(.v1_1)
250
	resp.set_status(http.status_from_int(ctx.status.int()))
251
	// send_string(mut ctx.conn, resp.bytestr()) or { return false }
252
	fast_send_resp(mut ctx.conn, resp) or { return false }
253
	return true
254
}
255

256
// Response with payload and content-type `text/html`
257
pub fn (mut ctx Context) html(payload string) Result {
258
	ctx.send_response_to_client('text/html', payload)
259
	return Result{}
260
}
261

262
// Response with s as payload and content-type `text/plain`
263
pub fn (mut ctx Context) text(s string) Result {
264
	ctx.send_response_to_client('text/plain', s)
265
	return Result{}
266
}
267

268
// Response with json_s as payload and content-type `application/json`
269
pub fn (mut ctx Context) json[T](j T) Result {
270
	json_s := json.encode(j)
271
	ctx.send_response_to_client('application/json', json_s)
272
	return Result{}
273
}
274

275
// Response with a pretty-printed JSON result
276
pub fn (mut ctx Context) json_pretty[T](j T) Result {
277
	json_s := json.encode_pretty(j)
278
	ctx.send_response_to_client('application/json', json_s)
279
	return Result{}
280
}
281

282
// TODO: test
283
// Response with file as payload
284
pub fn (mut ctx Context) file(f_path string) Result {
285
	if !os.exists(f_path) {
286
		eprintln('[vweb] file ${f_path} does not exist')
287
		return ctx.not_found()
288
	}
289
	ext := os.file_ext(f_path)
290
	data := os.read_file(f_path) or {
291
		eprint(err.msg())
292
		ctx.server_error(500)
293
		return Result{}
294
	}
295
	content_type := mime_types[ext]
296
	if content_type.len == 0 {
297
		eprintln('[vweb] no MIME type found for extension ${ext}')
298
		ctx.server_error(500)
299
	} else {
300
		ctx.send_response_to_client(content_type, data)
301
	}
302
	return Result{}
303
}
304

305
// Response with s as payload and sets the status code to HTTP_OK
306
pub fn (mut ctx Context) ok(s string) Result {
307
	ctx.set_status(200, 'OK')
308
	ctx.send_response_to_client(ctx.content_type, s)
309
	return Result{}
310
}
311

312
// TODO: test
313
// Response a server error
314
pub fn (mut ctx Context) server_error(ecode int) Result {
315
	$if debug {
316
		eprintln('> ctx.server_error ecode: ${ecode}')
317
	}
318
	if ctx.done {
319
		return Result{}
320
	}
321
	send_string(mut ctx.conn, http_500.bytestr()) or {}
322
	return Result{}
323
}
324

325
@[params]
326
pub struct RedirectParams {
327
pub:
328
	status_code int = 302
329
}
330

331
// Redirect to an url
332
pub fn (mut ctx Context) redirect(url string, params RedirectParams) Result {
333
	if ctx.done {
334
		return Result{}
335
	}
336
	ctx.done = true
337
	mut resp := http_302
338
	if params.status_code == 303 {
339
		resp = http_303
340
	}
341
	resp.header = resp.header.join(ctx.header)
342
	resp.header.add(.location, url)
343
	send_string(mut ctx.conn, resp.bytestr()) or { return Result{} }
344
	return Result{}
345
}
346

347
// Send an not_found response
348
pub fn (mut ctx Context) not_found() Result {
349
	// TODO: add a [must_be_returned] attribute, so that the caller is forced to use `return app.not_found()`
350
	if ctx.done {
351
		return Result{}
352
	}
353
	ctx.done = true
354
	send_string(mut ctx.conn, http_404.bytestr()) or {}
355
	return Result{}
356
}
357

358
// TODO: test
359
// Sets a cookie
360
pub fn (mut ctx Context) set_cookie(cookie http.Cookie) {
361
	cookie_raw := cookie.str()
362
	if cookie_raw == '' {
363
		eprintln('[vweb] error setting cookie: name of cookie is invalid')
364
		return
365
	}
366
	ctx.add_header('Set-Cookie', cookie_raw)
367
}
368

369
// Sets the response content type
370
pub fn (mut ctx Context) set_content_type(typ string) {
371
	ctx.content_type = typ
372
}
373

374
// TODO: test
375
// Sets a cookie with a `expire_date`
376
pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) {
377
	cookie := http.Cookie{
378
		name:    key
379
		value:   val
380
		expires: expire_date
381
	}
382
	ctx.set_cookie(cookie)
383
}
384

385
// Gets a cookie by a key
386
pub fn (ctx &Context) get_cookie(key string) !string {
387
	c := ctx.req.cookie(key) or { return error('Cookie not found') }
388
	return c.value
389
	// if value := ctx.req.cookies[key] {
390
	// return value
391
	//}
392
}
393

394
// TODO: test
395
// Sets the response status
396
pub fn (mut ctx Context) set_status(code int, desc string) {
397
	if code < 100 || code > 599 {
398
		ctx.status = '500 Internal Server Error'
399
	} else {
400
		ctx.status = '${code} ${desc}'
401
	}
402
}
403

404
// TODO: test
405
// Adds an header to the response with key and val
406
pub fn (mut ctx Context) add_header(key string, val string) {
407
	ctx.header.add_custom(key, val) or {}
408
}
409

410
// TODO: test
411
// Returns the header data from the key
412
pub fn (ctx &Context) get_header(key string) string {
413
	return ctx.req.header.get_custom(key) or { '' }
414
}
415

416
// set_value sets a value on the context
417
pub fn (mut ctx Context) set_value(key context.Key, value context.Any) {
418
	ctx.ctx = context.with_value(ctx.ctx, key, value)
419
}
420

421
// get_value gets a value from the context
422
pub fn (ctx &Context) get_value[T](key context.Key) ?T {
423
	if val := ctx.ctx.value(key) {
424
		match val {
425
			T {
426
				// `context.value()` always returns a reference
427
				// if we send back `val` the returntype becomes `?&T` and this can be problematic
428
				// for end users since they won't be able to do something like
429
				// `app.get_value[string]('a') or { '' }
430
				// since V expects the value in the or block to be of type `&string`.
431
				// And if a reference was allowed it would enable mutating the context directly
432
				return *val
433
			}
434
			else {}
435
		}
436
	}
437
	return none
438
}
439

440
pub type DatabasePool[T] = fn (tid int) T
441

442
interface DbPoolInterface {
443
	db_handle voidptr
444
mut:
445
	db voidptr
446
}
447

448
interface DbInterface {
449
mut:
450
	db voidptr
451
}
452

453
pub type Middleware = fn (mut Context) bool
454

455
interface MiddlewareInterface {
456
	middlewares map[string][]Middleware
457
}
458

459
// Generate route structs for an app
460
fn generate_routes[T](app &T) !map[string]Route {
461
	// Parsing methods attributes
462
	mut routes := map[string]Route{}
463
	$for method in T.methods {
464
		http_methods, route_path, middleware, host := parse_attrs(method.name, method.attrs) or {
465
			return error('error parsing method attributes: ${err}')
466
		}
467

468
		routes[method.name] = Route{
469
			methods:    http_methods
470
			path:       route_path
471
			path_words: route_path.split('/').filter(it != '')
472
			middleware: middleware
473
			host:       host
474
		}
475
	}
476
	return routes
477
}
478

479
type ControllerHandler = fn (ctx Context, mut url urllib.URL, host string, tid int)
480

481
pub struct ControllerPath {
482
pub:
483
	path    string
484
	handler ControllerHandler = unsafe { nil }
485
pub mut:
486
	host string
487
}
488

489
interface ControllerInterface {
490
	controllers []&ControllerPath
491
}
492

493
pub struct Controller {
494
pub mut:
495
	controllers []&ControllerPath
496
}
497

498
// controller generates a new Controller for the main app
499
pub fn controller[T](path string, global_app &T) &ControllerPath {
500
	routes := generate_routes(global_app) or { panic(err.msg()) }
501

502
	// generate struct with closure so the generic type is encapsulated in the closure
503
	// no need to type `ControllerHandler` as generic since it's not needed for closures
504
	return &ControllerPath{
505
		path:    path
506
		handler: fn [global_app, path, routes] [T](ctx Context, mut url urllib.URL, host string, tid int) {
507
			// request_app is freed in `handle_route`
508
			mut request_app := new_request_app[T](global_app, ctx, tid)
509
			// transform the url
510
			url.path = url.path.all_after_first(path)
511
			handle_route[T](mut request_app, url, host, &routes, tid)
512
		}
513
	}
514
}
515

516
// controller_host generates a controller which only handles incoming requests from the `host` domain
517
pub fn controller_host[T](host string, path string, global_app &T) &ControllerPath {
518
	mut ctrl := controller(path, global_app)
519
	ctrl.host = host
520
	return ctrl
521
}
522

523
// run - start a new VWeb server, listening to all available addresses, at the specified `port`
524
pub fn run[T](global_app &T, port int) {
525
	run_at[T](global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
526
}
527

528
@[params]
529
pub struct RunParams {
530
pub:
531
	family               net.AddrFamily = .ip6 // use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
532
	host                 string
533
	port                 int  = 8080
534
	nr_workers           int  = runtime.nr_jobs()
535
	pool_channel_slots   int  = 1000
536
	show_startup_message bool = true
537
	startup_message      string
538
}
539

540
// run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port`
541
// Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
542
@[manualfree]
543
pub fn run_at[T](global_app &T, params RunParams) ! {
544
	if params.port <= 0 || params.port > 65535 {
545
		return error('invalid port number `${params.port}`, it should be between 1 and 65535')
546
	}
547
	if params.pool_channel_slots < 1 {
548
		return error('invalid pool_channel_slots `${params.pool_channel_slots}`, it should be above 0, preferably higher than 10 x nr_workers')
549
	}
550
	if params.nr_workers < 1 {
551
		return error('invalid nr_workers `${params.nr_workers}`, it should be above 0')
552
	}
553

554
	routes := generate_routes(global_app)!
555
	controllers_sorted := check_duplicate_routes_in_controllers[T](global_app, routes)!
556

557
	listen_address := '${params.host}:${params.port}'
558
	mut l := net.listen_tcp(params.family, listen_address) or {
559
		ecode := err.code()
560
		return error('failed to listen ${ecode} ${err}')
561
	}
562
	$if trace_listen ? {
563
		eprintln('>> vweb listen_address: `${listen_address}` | params.family: ${params.family} | l.addr: ${l.addr()} | params: ${params}')
564
	}
565

566
	if params.show_startup_message {
567
		if params.startup_message == '' {
568
			host := if params.host == '' { 'localhost' } else { params.host }
569
			println('[Vweb] Running app on http://${host}:${params.port}/')
570
		} else {
571
			println(params.startup_message)
572
		}
573
	}
574

575
	ch := chan &RequestParams{cap: params.pool_channel_slots}
576
	mut ws := []thread{cap: params.nr_workers}
577
	for worker_number in 0 .. params.nr_workers {
578
		ws << new_worker[T](ch, worker_number)
579
	}
580
	if params.show_startup_message {
581
		println('[Vweb] We have ${ws.len} workers')
582
	}
583
	flush_stdout()
584

585
	unsafe {
586
		global_app.before_accept_loop()
587
	}
588

589
	// Forever accept every connection that comes, and
590
	// pass it through the channel, to the thread pool:
591
	for {
592
		mut connection := l.accept_only() or {
593
			// failures should not panic
594
			eprintln('[vweb] accept() failed with error: ${err.msg()}')
595
			continue
596
		}
597
		ch <- &RequestParams{
598
			connection:  connection
599
			global_app:  unsafe { global_app }
600
			controllers: controllers_sorted
601
			routes:      &routes
602
		}
603
	}
604
}
605

606
fn check_duplicate_routes_in_controllers[T](global_app &T, routes map[string]Route) ![]&ControllerPath {
607
	mut controllers_sorted := []&ControllerPath{}
608
	$if T is ControllerInterface {
609
		mut paths := []string{}
610
		controllers_sorted = global_app.controllers.clone()
611
		controllers_sorted.sort(a.path.len > b.path.len)
612
		for controller in controllers_sorted {
613
			if controller.host == '' {
614
				if controller.path in paths {
615
					return error('conflicting paths: duplicate controller handling the route "${controller.path}"')
616
				}
617
				paths << controller.path
618
			}
619
		}
620
		for method_name, route in routes {
621
			for controller_path in paths {
622
				if route.path.starts_with(controller_path) {
623
					return error('conflicting paths: method "${method_name}" with route "${route.path}" should be handled by the Controller of path "${controller_path}"')
624
				}
625
			}
626
		}
627
	}
628
	return controllers_sorted
629
}
630

631
fn new_request_app[T](global_app &T, ctx Context, tid int) &T {
632
	// Create a new app object for each connection, copy global data like db connections
633
	mut request_app := &T{}
634
	$if T is MiddlewareInterface {
635
		request_app = &T{
636
			middlewares: global_app.middlewares.clone()
637
		}
638
	}
639

640
	$if T is DbPoolInterface {
641
		// get database connection from the connection pool
642
		request_app.db = global_app.db_handle(tid)
643
	} $else $if T is DbInterface {
644
		// copy a database to a app without pooling
645
		request_app.db = global_app.db
646
	}
647

648
	$for field in T.fields {
649
		if field.is_shared {
650
			unsafe {
651
				// TODO: remove this horrible hack, when copying a shared field at comptime works properly!!!
652
				raptr := &voidptr(&request_app.$(field.name))
653
				gaptr := &voidptr(&global_app.$(field.name))
654
				*raptr = *gaptr
655
				_ = raptr // TODO: v produces a warning that `raptr` is unused otherwise, even though it was on the previous line
656
			}
657
		} else {
658
			if 'vweb_global' in field.attrs {
659
				request_app.$(field.name) = global_app.$(field.name)
660
			}
661
		}
662
	}
663
	request_app.Context = ctx // copy request data such as form and query etc
664

665
	// copy static files
666
	request_app.Context.static_files = global_app.static_files.clone()
667
	request_app.Context.static_mime_types = global_app.static_mime_types.clone()
668
	request_app.Context.static_hosts = global_app.static_hosts.clone()
669

670
	return request_app
671
}
672

673
@[manualfree]
674
fn handle_conn[T](mut conn net.TcpConn, global_app &T, controllers []&ControllerPath, routes &map[string]Route,
675
	tid int) {
676
	conn.set_read_timeout(30 * time.second)
677
	conn.set_write_timeout(30 * time.second)
678
	defer {
679
		conn.close() or {}
680
	}
681

682
	conn.set_sock() or {
683
		eprintln('[vweb] tid: ${tid:03d}, error setting socket')
684
		return
685
	}
686

687
	mut reader := io.new_buffered_reader(reader: conn)
688
	defer {
689
		unsafe {
690
			reader.free()
691
		}
692
	}
693

694
	page_gen_start := time.ticks()
695

696
	// Request parse
697
	req := http.parse_request(mut reader) or {
698
		// Prevents errors from being thrown when BufferedReader is empty
699
		if err !is io.Eof {
700
			eprintln('[vweb] tid: ${tid:03d}, error parsing request: ${err}')
701
		}
702
		return
703
	}
704
	$if trace_request ? {
705
		dump(req)
706
	}
707
	$if trace_request_url ? {
708
		dump(req.url)
709
	}
710
	// URL Parse
711
	mut url := urllib.parse(req.url) or {
712
		eprintln('[vweb] tid: ${tid:03d}, error parsing path: ${err}')
713
		return
714
	}
715

716
	// Query parse
717
	query := parse_query_from_url(url)
718

719
	// Form parse
720
	form, files := parse_form_from_request(req) or {
721
		// Bad request
722
		conn.write(http_400.bytes()) or {}
723
		return
724
	}
725

726
	// remove the port from the HTTP Host header
727
	host_with_port := req.header.get(.host) or { '' }
728
	host, _ := urllib.split_host_port(host_with_port)
729

730
	// Create Context with request data
731
	ctx := Context{
732
		ctx:            context.background()
733
		req:            req
734
		page_gen_start: page_gen_start
735
		conn:           conn
736
		query:          query
737
		form:           form
738
		files:          files
739
	}
740

741
	// match controller paths
742
	$if T is ControllerInterface {
743
		for controller in controllers {
744
			// skip controller if the hosts don't match
745
			if controller.host != '' && host != controller.host {
746
				continue
747
			}
748
			if url.path.len >= controller.path.len && url.path.starts_with(controller.path) {
749
				// pass route handling to the controller
750
				controller.handler(ctx, mut url, host, tid)
751
				return
752
			}
753
		}
754
	}
755

756
	mut request_app := new_request_app(global_app, ctx, tid)
757
	handle_route(mut request_app, url, host, routes, tid)
758
}
759

760
@[manualfree]
761
fn handle_route[T](mut app T, url urllib.URL, host string, routes &map[string]Route, tid int) {
762
	defer {
763
		unsafe {
764
			free(app)
765
		}
766
	}
767

768
	url_words := url.path.split('/').filter(it != '')
769

770
	// Calling middleware...
771
	app.before_request()
772

773
	$if vweb_livereload ? {
774
		if url.path.starts_with('/vweb_livereload/') {
775
			if url.path.ends_with('current') {
776
				app.handle_vweb_livereload_current()
777
				return
778
			}
779
			if url.path.ends_with('script.js') {
780
				app.handle_vweb_livereload_script()
781
				return
782
			}
783
		}
784
	}
785

786
	// Static handling
787
	if serve_if_static[T](mut app, url, host) {
788
		// successfully served a static file
789
		return
790
	}
791

792
	// Route matching
793
	$for method in T.methods {
794
		$if method.return_type is Result {
795
			route := (*routes)[method.name] or {
796
				eprintln('[vweb] tid: ${tid:03d}, parsed attributes for the `${method.name}` are not found, skipping...')
797
				Route{}
798
			}
799

800
			// Skip if the HTTP request method does not match the attributes
801
			if app.req.method in route.methods {
802
				// Used for route matching
803
				route_words := route.path_words // route.path.split('/').filter(it != '')
804
				// println('ROUTES ${routes}')
805
				// println('\nROUTE WORDS')
806
				// println(route_words)
807
				// println(route.path_words)
808

809
				// Skip if the host does not match or is empty
810
				if route.host == '' || route.host == host {
811
					// Route immediate matches first
812
					// For example URL `/register` matches route `/:user`, but `fn register()`
813
					// should be called first.
814
					if !route.path.contains('/:') && url_words == route_words {
815
						// We found a match
816
						$if T is MiddlewareInterface {
817
							if validate_middleware(mut app, url.path) == false {
818
								return
819
							}
820
						}
821

822
						if app.req.method == .post && method.args.len > 0 {
823
							// Populate method args with form values
824
							mut args := []string{cap: method.args.len}
825
							for param in method.args {
826
								args << app.form[param.name]
827
							}
828

829
							if route.middleware == '' {
830
								app.$method(args)
831
							} else if validate_app_middleware(mut app, route.middleware,
832
								method.name)
833
							{
834
								app.$method(args)
835
							}
836
						} else {
837
							if route.middleware == '' {
838
								app.$method()
839
							} else if validate_app_middleware(mut app, route.middleware,
840
								method.name)
841
							{
842
								app.$method()
843
							}
844
						}
845
						return
846
					}
847

848
					if url_words.len == 0 && route_words == ['index'] && method.name == 'index' {
849
						$if T is MiddlewareInterface {
850
							if validate_middleware(mut app, url.path) == false {
851
								return
852
							}
853
						}
854
						if route.middleware == '' {
855
							app.$method()
856
						} else if validate_app_middleware(mut app, route.middleware, method.name) {
857
							app.$method()
858
						}
859
						return
860
					}
861

862
					if params := route_matches(url_words, route_words) {
863
						method_args := params.clone()
864
						if method_args.len != method.args.len {
865
							eprintln('[vweb] tid: ${tid:03d}, warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})')
866
						}
867

868
						$if T is MiddlewareInterface {
869
							if validate_middleware(mut app, url.path) == false {
870
								return
871
							}
872
						}
873
						if route.middleware == '' {
874
							app.$method(method_args)
875
						} else if validate_app_middleware(mut app, route.middleware, method.name) {
876
							app.$method(method_args)
877
						}
878
						return
879
					}
880
				}
881
			}
882
		}
883
	}
884
	// Route not found
885
	app.not_found()
886
}
887

888
// validate_middleware validates and fires all middlewares that are defined in the global app instance
889
fn validate_middleware[T](mut app T, full_path string) bool {
890
	for path, middleware_chain in app.middlewares {
891
		// only execute middleware if route.path starts with `path`
892
		if full_path.len >= path.len && full_path.starts_with(path) {
893
			// there is middleware for this route
894
			for func in middleware_chain {
895
				if func(mut app.Context) == false {
896
					return false
897
				}
898
			}
899
		}
900
	}
901
	// passed all middleware checks
902
	return true
903
}
904

905
// validate_app_middleware validates all middlewares as a method of `app`
906
fn validate_app_middleware[T](mut app T, middleware string, method_name string) bool {
907
	// then the middleware that is defined for this route specifically
908
	valid := fire_app_middleware(mut app, middleware) or {
909
		eprintln('[vweb] warning: middleware `${middleware}` for the `${method_name}` are not found')
910
		true
911
	}
912
	return valid
913
}
914

915
// fire_app_middleware fires all middlewares that are defined as a method of `app`
916
fn fire_app_middleware[T](mut app T, method_name string) ?bool {
917
	$for method in T.methods {
918
		if method_name == method.name {
919
			$if method.return_type is bool {
920
				return app.$method()
921
			} $else {
922
				eprintln('[vweb] error in `${method.name}, middleware functions must return bool')
923
				return none
924
			}
925
		}
926
	}
927
	// no middleware function found
928
	return none
929
}
930

931
fn route_matches(url_words []string, route_words []string) ?[]string {
932
	// URL path should be at least as long as the route path
933
	// except for the catchall route (`/:path...`)
934
	if route_words.len == 1 && route_words[0].starts_with(':') && route_words[0].ends_with('...') {
935
		return ['/' + url_words.join('/')]
936
	}
937
	if url_words.len < route_words.len {
938
		return none
939
	}
940

941
	mut params := []string{cap: url_words.len}
942
	if url_words.len == route_words.len {
943
		for i in 0 .. url_words.len {
944
			if route_words[i].starts_with(':') {
945
				// We found a path parameter
946
				params << url_words[i]
947
			} else if route_words[i] != url_words[i] {
948
				// This url does not match the route
949
				return none
950
			}
951
		}
952
		return params
953
	}
954

955
	// The last route can end with ... indicating an array
956
	if route_words.len == 0 || !route_words[route_words.len - 1].ends_with('...') {
957
		return none
958
	}
959

960
	for i in 0 .. route_words.len - 1 {
961
		if route_words[i].starts_with(':') {
962
			// We found a path parameter
963
			params << url_words[i]
964
		} else if route_words[i] != url_words[i] {
965
			// This url does not match the route
966
			return none
967
		}
968
	}
969
	params << url_words[route_words.len - 1..url_words.len].join('/')
970
	return params
971
}
972

973
// check if request is for a static file and serves it
974
// returns true if we served a static file, false otherwise
975
@[manualfree]
976
fn serve_if_static[T](mut app T, url urllib.URL, host string) bool {
977
	// TODO: handle url parameters properly - for now, ignore them
978
	static_file := app.static_files[url.path] or { return false }
979
	mime_type := app.static_mime_types[url.path] or { return false }
980
	static_host := app.static_hosts[url.path] or { '' }
981
	if static_file == '' || mime_type == '' {
982
		return false
983
	}
984
	if static_host != '' && static_host != host {
985
		return false
986
	}
987
	data := os.read_file(static_file) or {
988
		send_string(mut app.conn, http_404.bytestr()) or {}
989
		return true
990
	}
991
	app.send_response_to_client(mime_type, data)
992
	unsafe { data.free() }
993
	return true
994
}
995

996
fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string, host string) {
997
	files := os.ls(directory_path) or { panic(err) }
998
	if files.len > 0 {
999
		for file in files {
1000
			full_path := os.join_path(directory_path, file)
1001
			if os.is_dir(full_path) {
1002
				ctx.scan_static_directory(full_path, mount_path.trim_right('/') + '/' + file,
1003
					host)
1004
			} else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') {
1005
				ext := os.file_ext(file)
1006
				// Rudimentary guard against adding files not in mime_types.
1007
				// Use host_serve_static directly to add non-standard mime types.
1008
				if ext in mime_types {
1009
					ctx.host_serve_static(host, mount_path.trim_right('/') + '/' + file,
1010
						full_path)
1011
				}
1012
			}
1013
		}
1014
	}
1015
}
1016

1017
// handle_static is used to mark a folder (relative to the current working folder)
1018
// as one that contains only static resources (css files, images etc).
1019
// If `root` is set the mount path for the dir will be in '/'
1020
// Usage:
1021
// ```v
1022
// os.chdir( os.executable() )?
1023
// app.handle_static('assets', true)
1024
// ```
1025
pub fn (mut ctx Context) handle_static(directory_path string, root bool) bool {
1026
	return ctx.host_handle_static('', directory_path, root)
1027
}
1028

1029
// host_handle_static is used to mark a folder (relative to the current working folder)
1030
// as one that contains only static resources (css files, images etc).
1031
// If `root` is set the mount path for the dir will be in '/'
1032
// Usage:
1033
// ```v
1034
// os.chdir( os.executable() )?
1035
// app.host_handle_static('localhost', 'assets', true)
1036
// ```
1037
pub fn (mut ctx Context) host_handle_static(host string, directory_path string, root bool) bool {
1038
	if ctx.done || !os.exists(directory_path) {
1039
		return false
1040
	}
1041
	dir_path := directory_path.trim_space().trim_right('/')
1042
	mut mount_path := ''
1043
	if dir_path != '.' && os.is_dir(dir_path) && !root {
1044
		// Mount point hygiene, "./assets" => "/assets".
1045
		mount_path = '/' + dir_path.trim_left('.').trim('/')
1046
	}
1047
	ctx.scan_static_directory(dir_path, mount_path, host)
1048
	return true
1049
}
1050

1051
// TODO: test
1052
// mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path
1053
// For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
1054
// and you have a file /var/share/myassets/main.css .
1055
// => That file will be available at URL: http://server/assets/main.css .
1056
pub fn (mut ctx Context) mount_static_folder_at(directory_path string, mount_path string) bool {
1057
	return ctx.host_mount_static_folder_at('', directory_path, mount_path)
1058
}
1059

1060
// TODO: test
1061
// host_mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://host/mount_path
1062
// For example: suppose you have called .host_mount_static_folder_at('localhost', '/var/share/myassets', '/assets'),
1063
// and you have a file /var/share/myassets/main.css .
1064
// => That file will be available at URL: http://localhost/assets/main.css .
1065
pub fn (mut ctx Context) host_mount_static_folder_at(host string, directory_path string, mount_path string) bool {
1066
	if ctx.done || mount_path == '' || mount_path[0] != `/` || !os.exists(directory_path) {
1067
		return false
1068
	}
1069
	dir_path := directory_path.trim_right('/')
1070

1071
	trim_mount_path := mount_path.trim_left('/').trim_right('/')
1072
	ctx.scan_static_directory(dir_path, '/${trim_mount_path}', host)
1073
	return true
1074
}
1075

1076
// TODO: test
1077
// Serves a file static
1078
// `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type
1079
pub fn (mut ctx Context) serve_static(url string, file_path string) {
1080
	ctx.host_serve_static('', url, file_path)
1081
}
1082

1083
// TODO: test
1084
// Serves a file static
1085
// `url` is the access path on the site, `file_path` is the real path to the file
1086
// `mime_type` is the file type, `host` is the host to serve the file from
1087
pub fn (mut ctx Context) host_serve_static(host string, url string, file_path string) {
1088
	ctx.static_files[url] = file_path
1089
	// ctx.static_mime_types[url] = mime_type
1090
	ext := os.file_ext(file_path)
1091
	ctx.static_mime_types[url] = mime_types[ext]
1092
	ctx.static_hosts[url] = host
1093
}
1094

1095
// user_agent returns the user-agent header for the current client
1096
pub fn (ctx &Context) user_agent() string {
1097
	return ctx.req.header.get(.user_agent) or { '' }
1098
}
1099

1100
// Returns the ip address from the current user
1101
pub fn (ctx &Context) ip() string {
1102
	mut ip := ctx.req.header.get(.x_forwarded_for) or { '' }
1103
	if ip == '' {
1104
		ip = ctx.req.header.get_custom('X-Real-Ip') or { '' }
1105
	}
1106

1107
	if ip.contains(',') {
1108
		ip = ip.all_before(',')
1109
	}
1110
	if ip == '' {
1111
		ip = ctx.conn.peer_ip() or { '' }
1112
	}
1113
	return ip
1114
}
1115

1116
// Set s to the form error
1117
pub fn (mut ctx Context) error(s string) {
1118
	eprintln('[vweb] Context.error: ${s}')
1119
	ctx.form_error = s
1120
	// ctx.set_cookie(name: 'veb.error', value: s)
1121
}
1122

1123
// Returns an empty result
1124
pub fn not_found() Result {
1125
	return Result{}
1126
}
1127

1128
fn send_string(mut conn net.TcpConn, s string) ! {
1129
	$if trace_send_string_conn ? {
1130
		eprintln('> send_string: conn: ${ptr_str(conn)}')
1131
	}
1132
	$if trace_response ? {
1133
		eprintln('> send_string:\n${s}\n')
1134
	}
1135
	if voidptr(conn) == unsafe { nil } {
1136
		return error('connection was closed before send_string')
1137
	}
1138
	conn.write_string(s)!
1139
}
1140

1141
// Formats resp to a string suitable for HTTP response transmission
1142
// A fast version of `resp.bytestr()` used with
1143
// `send_string(mut ctx.conn, resp.bytestr())`
1144
fn fast_send_resp(mut conn net.TcpConn, resp http.Response) ! {
1145
	mut sb := strings.new_builder(resp.body.len + 200)
1146
	/*
1147
	send_string(mut conn, 'HTTP/')!
1148
	send_string(mut conn, resp.http_version)!
1149
	send_string(mut conn, ' ')!
1150
	send_string(mut conn, resp.status_code.str())!
1151
	send_string(mut conn, ' ')!
1152
	send_string(mut conn, resp.status_msg)!
1153
	send_string(mut conn, '\r\n')!
1154
	send_string(mut conn, resp.header.render(
1155
		version: resp.version()
1156
	))!
1157
	send_string(mut conn, '\r\n')!
1158
	send_string(mut conn, resp.body)!
1159
	*/
1160
	sb.write_string('HTTP/')
1161
	sb.write_string(resp.http_version)
1162
	sb.write_string(' ')
1163
	sb.write_decimal(resp.status_code)
1164
	sb.write_string(' ')
1165
	sb.write_string(resp.status_msg)
1166
	sb.write_string('\r\n')
1167
	// sb.write_string(resp.header.render_with_sb(
1168
	// version: resp.version()
1169
	//))
1170
	resp.header.render_into_sb(mut sb,
1171
		version: resp.version()
1172
	)
1173
	sb.write_string('\r\n')
1174
	sb.write_string(resp.body)
1175
	send_string(mut conn, sb.str())!
1176
}
1177

1178
// Do not delete.
1179
// It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside vweb templates
1180
// TODO: move it to template render
1181
fn filter(s string) string {
1182
	return html.escape(s)
1183
}
1184

1185
// Worker functions for the thread pool:
1186
struct RequestParams {
1187
	global_app  voidptr
1188
	controllers []&ControllerPath
1189
	routes      &map[string]Route
1190
mut:
1191
	connection &net.TcpConn
1192
}
1193

1194
struct Worker[T] {
1195
	id int
1196
	ch chan &RequestParams
1197
}
1198

1199
fn new_worker[T](ch chan &RequestParams, id int) thread {
1200
	mut w := &Worker[T]{
1201
		id: id
1202
		ch: ch
1203
	}
1204
	return spawn w.process_incoming_requests[T]()
1205
}
1206

1207
fn (mut w Worker[T]) process_incoming_requests() {
1208
	sid := '[vweb] tid: ${w.id:03d} received request'
1209
	for {
1210
		mut params := <-w.ch or { break }
1211
		$if vweb_trace_worker_scan ? {
1212
			eprintln(sid)
1213
		}
1214
		handle_conn[T](mut params.connection, params.global_app, params.controllers, params.routes,
1215
			w.id)
1216
	}
1217
	$if vweb_trace_worker_scan ? {
1218
		eprintln('[vweb] closing worker ${w.id}.')
1219
	}
1220
}
1221

1222
@[params]
1223
pub struct PoolParams[T] {
1224
pub:
1225
	handler    fn () T = unsafe { nil } @[required]
1226
	nr_workers int     = runtime.nr_jobs()
1227
}
1228

1229
// database_pool creates a pool of database connections
1230
pub fn database_pool[T](params PoolParams[T]) DatabasePool[T] {
1231
	mut connections := []T{}
1232
	// create a database connection for each worker
1233
	for _ in 0 .. params.nr_workers {
1234
		connections << params.handler()
1235
	}
1236

1237
	return fn [connections] [T](tid int) T {
1238
		$if vweb_trace_worker_scan ? {
1239
			eprintln('[vweb] worker ${tid} received database connection')
1240
		}
1241
		return connections[tid]
1242
	}
1243
}
1244

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.