diff --git a/examples/vweb/middleware/templates/base.html b/examples/vweb/middleware/templates/base.html new file mode 100644 index 0000000000..43c947661f --- /dev/null +++ b/examples/vweb/middleware/templates/base.html @@ -0,0 +1,21 @@ + + +
+ + + +Early exit
\ No newline at end of file diff --git a/examples/vweb/middleware/templates/index.html b/examples/vweb/middleware/templates/index.html new file mode 100644 index 0000000000..31db2b0ea8 --- /dev/null +++ b/examples/vweb/middleware/templates/index.html @@ -0,0 +1,3 @@ + + +Super secret stuff
\ No newline at end of file diff --git a/examples/vweb/middleware/vweb_example.v b/examples/vweb/middleware/vweb_example.v new file mode 100644 index 0000000000..65430bd8d8 --- /dev/null +++ b/examples/vweb/middleware/vweb_example.v @@ -0,0 +1,109 @@ +module main + +import vweb + +// for another example see vlib/vweb/tests/middleware_test_server.v +const ( + http_port = 8080 +) + +struct App { + vweb.Context + middlewares map[string][]vweb.Middleware +mut: + is_authenticated bool +} + +fn main() { + mut app := new_app() + vweb.run(app, http_port) +} + +fn new_app() &App { + mut app := &App{ + middlewares: { + // chaining is allowed, middleware will be evaluated in order + '/admin/': [other_func1, other_func2] + '/early': [middleware_early] + } + } + + // do stuff with app + // ... + return app +} + +['/'] +pub fn (mut app App) index() vweb.Result { + println('Index page') + title := 'Home Page' + + content := $tmpl('templates/index.html') + base := $tmpl('templates/base.html') + return app.html(base) +} + +[middleware: check_auth] +['/admin/secrets'] +pub fn (mut app App) secrets() vweb.Result { + println('Secrets page') + title := 'Secret Admin Page' + + content := $tmpl('templates/secret.html') + base := $tmpl('templates/base.html') + return app.html(base) +} + +['/admin/:sub'] +pub fn (mut app App) dynamic(sub string) vweb.Result { + println('Dynamic page') + title := 'Secret dynamic' + + content := sub + base := $tmpl('templates/base.html') + return app.html(base) +} + +['/early'] +pub fn (mut app App) early() vweb.Result { + println('Early page') + title := 'Early Exit' + + content := $tmpl('templates/early.html') + base := $tmpl('templates/base.html') + return app.html(base) +} + +// is always executed first! +pub fn (mut app App) before_request() { + app.is_authenticated = false + println('0') +} + +pub fn (mut app App) check_auth() bool { + println('3') + if app.is_authenticated == false { + app.redirect('/') + } + return app.is_authenticated +} + +fn other_func1(mut ctx vweb.Context) bool { + println('1') + return true +} + +fn other_func2(mut ctx vweb.Context) bool { + println('2') + + // ... + return true +} + +fn middleware_early(mut ctx vweb.Context) bool { + println('4') + ctx.text(':(') + + // returns false, so the middleware propogation is stopped and the user will see the text ":(" + return false +} diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index 56d3502987..e7ad29eb46 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -252,9 +252,10 @@ pub fn (mut app App) controller_get_user_by_id() vweb.Result { ``` ### Middleware -V haven't a well defined middleware. -For now, you can use `before_request()`. This method called before every request. -Probably you can use it for check user session cookie or add header +Vweb has different kinds of middleware. +The `before_request()` method is always called before every request before any +other middleware is processed. You could use it to check user session cookies or to add a header. + **Example:** ```v ignore @@ -263,24 +264,117 @@ pub fn (mut app App) before_request() { } ``` +Middleware functions can be passed directly when creating an App instance and is +executed when the url starts with the defined key. + +In the following example, if a user navigates to `/path/to/test` the middleware +is executed in the following order: `middleware_func`, `other_func`, `global_middleware`. +The middleware is executed in the same order as they are defined and if any function in +the chain returns `false` the propogation is stopped. + +**Example:** +```v +module main + +import vweb + +struct App { + vweb.Context + middlewares map[string][]vweb.Middleware +} + +fn new_app() &App { + mut app := &App{ + middlewares: { + // chaining is allowed, middleware will be evaluated in order + '/path/to/': [middleware_func, other_func] + '/': [global_middleware] + } + } + + // do stuff with app + // ... + return app +} + +fn middleware_func(mut ctx vweb.Context) bool { + // ... + return true +} + +fn other_func(mut ctx vweb.Context) bool { + // ... + return true +} + +fn global_middleware(mut ctx vweb.Context) bool { + // ... + return true +} +``` + +Middleware functions will be of type `vweb.Middleware` and are not methods of App, +so they could also be imported from other modules. +```v ignore +pub type Middleware = fn (mut Context) bool +``` + +Middleware can also be added to route specific functions via attributes. + +**Example:** +```v ignore +[middleware: check_auth] +['/admin/data'] +pub fn (mut app App) admin() vweb.Result { + // ... +} + +// check_auth is a method of App, so we don't need to pass the context as parameter. +pub fn (mut app App) check_auth () bool { + // ... + return true +} +``` +For now you can only add 1 middleware to a route specific function via attributes. + ### Redirect Used when you want be redirected to an url + **Examples:** ```v ignore pub fn (mut app App) before_request() { - app.user_id = app.get_cookie('id') or { app.redirect('/') } + app.user_id = app.get_cookie('id') or { app.redirect('/') } } ``` ```v ignore ['/articles'; get] pub fn (mut app App) articles() vweb.Result { - if !app.token { - app.redirect('/login') - } - return app.text("patatoes") + if !app.token { + app.redirect('/login') + } + return app.text('patatoes') +} +``` + +You can also combine middleware and redirect. + +**Example:** + +```v ignore +[middleware: with_auth] +['/admin/secret'] +pub fn (mut app App) admin_secret() vweb.Result { + // this code should never be reached + return app.text('secret') +} + +['/redirect'] +pub fn (mut app App) with_auth() bool { + app.redirect('/auth/login') + return false } ``` diff --git a/vlib/vweb/parse.v b/vlib/vweb/parse.v index f164ff1647..a191eb9131 100644 --- a/vlib/vweb/parse.v +++ b/vlib/vweb/parse.v @@ -4,13 +4,14 @@ import net.urllib import net.http // Parsing function attributes for methods and path. -fn parse_attrs(name string, attrs []string) !([]http.Method, string) { +fn parse_attrs(name string, attrs []string) !([]http.Method, string, string) { if attrs.len == 0 { - return [http.Method.get], '/${name}' + return [http.Method.get], '/${name}', '' } mut x := attrs.clone() mut methods := []http.Method{} + mut middleware := '' mut path := '' for i := 0; i < x.len; { @@ -30,6 +31,11 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) { x.delete(i) continue } + if attr.starts_with('middleware:') { + middleware = attr.all_after('middleware:').trim_space() + x.delete(i) + continue + } i++ } if x.len > 0 { @@ -44,7 +50,7 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) { path = '/${name}' } // Make path lowercase for case-insensitive comparisons - return methods, path.to_lower() + return methods, path.to_lower(), middleware } fn parse_query_from_url(url urllib.URL) map[string]string { diff --git a/vlib/vweb/tests/middleware_test.v b/vlib/vweb/tests/middleware_test.v new file mode 100644 index 0000000000..ff468fd3e0 --- /dev/null +++ b/vlib/vweb/tests/middleware_test.v @@ -0,0 +1,350 @@ +import os +import time +import json +import net +import net.http +import io + +const ( + sport = 12381 + localserver = '127.0.0.1:${sport}' + exit_after_time = 12000 // milliseconds + vexe = os.getenv('VEXE') + vweb_logfile = os.getenv('VWEB_LOGFILE') + vroot = os.dir(vexe) + serverexe = os.join_path(os.cache_dir(), 'middleware_test_server.exe') + tcp_r_timeout = 30 * time.second + tcp_w_timeout = 30 * time.second +) + +// setup of vweb webserver +fn testsuite_begin() { + os.chdir(vroot) or {} + if os.exists(serverexe) { + os.rm(serverexe) or {} + } +} + +fn test_middleware_vweb_app_can_be_compiled() { + // did_server_compile := os.system('${os.quoted_path(vexe)} -g -o ${os.quoted_path(serverexe)} vlib/vweb/tests/middleware_test_server.vv') + // TODO: find out why it does not compile with -usecache and -g + did_server_compile := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(serverexe)} vlib/vweb/tests/middleware_test_server.v') + assert did_server_compile == 0 + assert os.exists(serverexe) +} + +fn test_middleware_vweb_app_runs_in_the_background() { + mut suffix := '' + $if !windows { + suffix = ' > /dev/null &' + } + if vweb_logfile != '' { + suffix = ' 2>> ${os.quoted_path(vweb_logfile)} >> ${os.quoted_path(vweb_logfile)} &' + } + server_exec_cmd := '${os.quoted_path(serverexe)} ${sport} ${exit_after_time} ${suffix}' + $if debug_net_socket_client ? { + eprintln('running:\n${server_exec_cmd}') + } + $if windows { + spawn os.system(server_exec_cmd) + } $else { + res := os.system(server_exec_cmd) + assert res == 0 + } + $if macos { + time.sleep(1000 * time.millisecond) + } $else { + time.sleep(100 * time.millisecond) + } +} + +// normal routes: + +fn test_app_middleware() { + x := http.get('http://${localserver}/') or { panic(err) } + assert x.body == '0app_middlewareindex' +} + +fn test_single_middleware() { + received := simple_tcp_client(path: '/single') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1HTTP/') + assert received.ends_with('0single') +} + +fn test_multiple_middleware() { + received := simple_tcp_client(path: '/multiple') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1m2HTTP/') + assert received.ends_with('0multiple') +} + +fn test_combined_middleware() { + received := simple_tcp_client(path: '/combined') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1m2HTTP/') + assert received.ends_with('0app_middlewarecombined') +} + +fn test_nested_middleware() { + received := simple_tcp_client(path: '/admin/nested') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1HTTP/') + assert received.ends_with('0nested') +} + +// above routes + post + +struct Post { + msg string +} + +fn test_app_post_middleware() { + test_object := Post{ + msg: 'HI' + } + json_test := json.encode(test_object) + mut x := http.post_json('http://${localserver}/index_post', json_test) or { panic(err) } + assert x.body == '0app_middlewareindex_post:${json_test}' +} + +fn test_single_post_middleware() { + test_object := Post{ + msg: 'HI' + } + json_test := json.encode(test_object) + + received := simple_tcp_client_post_json( + path: '/single_post' + headers: 'Content-Length: ${json_test.len}\r\n' + content: json_test + ) or { + assert err.msg() == '' + return + } + + assert received.starts_with('m1') + assert received.ends_with('0single_post:${json_test}') +} + +fn test_multiple_post_middleware() { + test_object := Post{ + msg: 'HI' + } + json_test := json.encode(test_object) + + received := simple_tcp_client_post_json( + path: '/multiple_post' + headers: 'Content-Length: ${json_test.len}\r\n' + content: json_test + ) or { + assert err.msg() == '' + return + } + + assert received.starts_with('m1m2') + assert received.ends_with('0multiple_post:${json_test}') +} + +fn test_combined_post_middleware() { + test_object := Post{ + msg: 'HI' + } + json_test := json.encode(test_object) + + received := simple_tcp_client_post_json( + path: '/combined_post' + headers: 'Content-Length: ${json_test.len}\r\n' + content: json_test + ) or { + assert err.msg() == '' + return + } + assert received.starts_with('m1m2') + assert received.ends_with('0app_middlewarecombined_post:${json_test}') +} + +fn test_nested_post_middleware() { + test_object := Post{ + msg: 'HI' + } + json_test := json.encode(test_object) + + received := simple_tcp_client_post_json( + path: '/admin/nested_post' + headers: 'Content-Length: ${json_test.len}\r\n' + content: json_test + ) or { + assert err.msg() == '' + return + } + assert received.starts_with('m1') + assert received.ends_with('0nested_post:${json_test}') +} + +// dynamic routes: + +fn test_dynamic_middleware() { + dynamic_path := 'test' + received := simple_tcp_client(path: '/admin/${dynamic_path}') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1HTTP/') + assert received.ends_with('0admin_dynamic:${dynamic_path}') +} + +fn test_combined_dynamic_middleware() { + dynamic_path := 'test' + received := simple_tcp_client(path: '/other/${dynamic_path}') or { + assert err.msg() == '' + return + } + assert received.starts_with('m1m2HTTP/') + assert received.ends_with('0app_middlewarecombined_dynamic:${dynamic_path}') +} + +// redirect routes: + +fn test_app_redirect_middleware() { + x := http.get('http://${localserver}/app_redirect') or { panic(err) } + x_home := http.get('http://${localserver}/') or { panic(err) } + assert x.body == x_home.body + + received := simple_tcp_client(path: '/app_redirect') or { + assert err.msg() == '' + return + } + assert received.starts_with('HTTP/1.1 302 Found') + assert received.ends_with('302 Found') +} + +fn test_redirect_middleware() { + received := simple_tcp_client(path: '/redirect') or { + assert err.msg() == '' + return + } + println(received) + + assert received.starts_with('m_redirect') + assert received.contains('HTTP/1.1 302 Found') + assert received.ends_with('302 Found') +} + +fn testsuite_end() { + // This test is guaranteed to be called last. + // It sends a request to the server to shutdown. + x := http.fetch( + url: 'http://${localserver}/shutdown' + method: .get + cookies: { + 'skey': 'superman' + } + ) or { + assert err.msg() == '' + return + } + assert x.status() == .ok + assert x.body == 'good bye' +} + +// utility code: +struct SimpleTcpClientConfig { + retries int = 20 + host string = 'static.dev' + path string = '/' + agent string = 'v/net.tcp.v' + headers string = '\r\n' + content string +} + +fn simple_tcp_client(config SimpleTcpClientConfig) !string { + mut client := &net.TcpConn(0) + mut tries := 0 + for tries < config.retries { + tries++ + eprintln('> client retries: ${tries}') + client = net.dial_tcp(localserver) or { + if tries > config.retries { + return err + } + time.sleep(100 * time.millisecond) + continue + } + break + } + if client == unsafe { nil } { + eprintln('coult not create a tcp client connection to ${localserver} after ${config.retries} retries') + exit(1) + } + client.set_read_timeout(tcp_r_timeout) + client.set_write_timeout(tcp_w_timeout) + defer { + client.close() or {} + } + message := 'GET ${config.path} HTTP/1.1 +Host: ${config.host} +User-Agent: ${config.agent} +Accept: */* +${config.headers} +${config.content}' + $if debug_net_socket_client ? { + eprintln('sending:\n${message}') + } + client.write(message.bytes())! + read := io.read_all(reader: client)! + $if debug_net_socket_client ? { + eprintln('received:\n${read}') + } + return read.bytestr() +} + +fn simple_tcp_client_post_json(config SimpleTcpClientConfig) !string { + mut client := &net.TcpConn(0) + mut tries := 0 + for tries < config.retries { + tries++ + eprintln('> client retries: ${tries}') + client = net.dial_tcp(localserver) or { + if tries > config.retries { + return err + } + time.sleep(100 * time.millisecond) + continue + } + break + } + if client == unsafe { nil } { + eprintln('coult not create a tcp client connection to ${localserver} after ${config.retries} retries') + exit(1) + } + client.set_read_timeout(tcp_r_timeout) + client.set_write_timeout(tcp_w_timeout) + defer { + client.close() or {} + } + message := 'POST ${config.path} HTTP/1.1 +Host: ${config.host} +User-Agent: ${config.agent} +Accept: */* +Content-Type: application/json +${config.headers} +${config.content}' + $if debug_net_socket_client ? { + eprintln('sending:\n${message}') + } + client.write(message.bytes())! + read := io.read_all(reader: client)! + $if debug_net_socket_client ? { + eprintln('received:\n${read}') + } + return read.bytestr() +} diff --git a/vlib/vweb/tests/middleware_test_server.v b/vlib/vweb/tests/middleware_test_server.v new file mode 100644 index 0000000000..479be3986a --- /dev/null +++ b/vlib/vweb/tests/middleware_test_server.v @@ -0,0 +1,274 @@ +module main + +import vweb +import time +import os + +struct App { + vweb.Context + timeout int + global_config shared Config + middlewares map[string][]vweb.Middleware +} + +struct Config { +pub mut: + middleware_text string +} + +fn exit_after_timeout(timeout_in_ms int) { + time.sleep(timeout_in_ms * time.millisecond) + println('>> webserver: pid: ${os.getpid()}, exiting ...') + exit(0) +} + +fn main() { + if os.args.len != 3 { + panic('Usage: `vweb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`') + } + http_port := os.args[1].int() + assert http_port > 0 + timeout := os.args[2].int() + spawn exit_after_timeout(timeout) + + shared config := &Config{} + + app := &App{ + timeout: timeout + global_config: config + middlewares: { + '/single': [middleware1] + '/single_post': [middleware1] + '/multiple': [middleware1, middleware2] + '/multiple_post': [middleware1, middleware2] + '/combined': [middleware1, middleware2] + '/combined_post': [middleware1, middleware2] + '/admin/': [middleware1] + '/other/': [middleware1, middleware2] + '/redirect': [middleware_redirect] + } + } + eprintln('>> webserver: pid: ${os.getpid()}, started on http://localhost:${http_port}/ , with maximum runtime of ${app.timeout} milliseconds.') + vweb.run_at(app, host: 'localhost', port: http_port, family: .ip)! +} + +// normal routes: + +[middleware: app_middleware] +['/'] +pub fn (mut app App) index() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}index') +} + +['/single'] +pub fn (mut app App) single() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}single') +} + +['/multiple'] +pub fn (mut app App) multiple() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}multiple') +} + +[middleware: app_middleware] +['/combined'] +pub fn (mut app App) combined() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}combined') +} + +['/admin/nested'] +pub fn (mut app App) nested() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}nested') +} + +// above routes + post + +[middleware: app_middleware] +['/index_post'; post] +pub fn (mut app App) index_post() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}index_post:${app.req.data}') +} + +['/single_post'; post] +pub fn (mut app App) single_post() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}single_post:${app.req.data}') +} + +['/multiple_post'; post] +pub fn (mut app App) multiple_post() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}multiple_post:${app.req.data}') +} + +[middleware: app_middleware] +['/combined_post'; post] +pub fn (mut app App) combined_post() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}combined_post:${app.req.data}') +} + +['/admin/nested_post'; post] +pub fn (mut app App) nested_post() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}nested_post:${app.req.data}') +} + +// dynamic routes + +['/admin/:dynamic'] +pub fn (mut app App) admin_dynamic(dynamic string) vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}admin_dynamic:${dynamic}') +} + +[middleware: app_middleware] +['/other/:dynamic'] +pub fn (mut app App) combined_dynamic(dynamic string) vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}combined_dynamic:${dynamic}') +} + +// redirect routes: + +[middleware: app_redirect] +['/app_redirect'] +pub fn (mut app App) app_redirect_route() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}should_never_reach!') +} + +['/redirect'] +pub fn (mut app App) redirect_route() vweb.Result { + mut result := '' + + rlock app.global_config { + result = app.global_config.middleware_text + } + + return app.text('${result}should_never_reach!') +} + +// middleware functions: + +pub fn (mut app App) before_request() { + lock app.global_config { + app.global_config.middleware_text = '0' + } +} + +pub fn (mut app App) app_middleware() bool { + lock app.global_config { + app.global_config.middleware_text += 'app_middleware' + } + return true +} + +pub fn (mut app App) app_redirect() bool { + app.redirect('/') + return false +} + +fn middleware1(mut ctx vweb.Context) bool { + ctx.conn.write_string('m1') or { panic(err) } + return true +} + +fn middleware2(mut ctx vweb.Context) bool { + ctx.conn.write_string('m2') or { panic(err) } + return true +} + +fn middleware_redirect(mut ctx vweb.Context) bool { + ctx.conn.write_string('m_redirect') or { panic(err) } + ctx.redirect('/') + return false +} + +// utility functions: + +pub fn (mut app App) shutdown() vweb.Result { + session_key := app.get_cookie('skey') or { return app.not_found() } + if session_key != 'superman' { + return app.not_found() + } + spawn app.gracefull_exit() + return app.ok('good bye') +} + +fn (mut app App) gracefull_exit() { + eprintln('>> webserver: gracefull_exit') + time.sleep(100 * time.millisecond) + exit(0) +} diff --git a/vlib/vweb/tests/vweb_test.v b/vlib/vweb/tests/vweb_test.v index a82e61401d..2cb62094e7 100644 --- a/vlib/vweb/tests/vweb_test.v +++ b/vlib/vweb/tests/vweb_test.v @@ -7,7 +7,7 @@ import io const ( sport = 12380 - localserver = 'localhost:${sport}' + localserver = '127.0.0.1:${sport}' exit_after_time = 12000 // milliseconds vexe = os.getenv('VEXE') vweb_logfile = os.getenv('VWEB_LOGFILE') diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index a06520b998..c695c746e7 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -179,8 +179,9 @@ pub: } struct Route { - methods []http.Method - path string + methods []http.Method + path string + middleware string } // Defining this method is optional. @@ -383,6 +384,12 @@ interface DbInterface { db voidptr } +pub type Middleware = fn (mut Context) bool + +interface MiddlewareInterface { + middlewares map[string][]Middleware +} + // run - start a new VWeb server, listening to all available addresses, at the specified `port` pub fn run[T](global_app &T, port int) { run_at[T](global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) } @@ -411,13 +418,14 @@ pub fn run_at[T](global_app &T, params RunParams) ! { // Parsing methods attributes mut routes := map[string]Route{} $for method in T.methods { - http_methods, route_path := parse_attrs(method.name, method.attrs) or { + http_methods, route_path, middleware := parse_attrs(method.name, method.attrs) or { return error('error parsing method attributes: ${err}') } routes[method.name] = Route{ methods: http_methods path: route_path + middleware: middleware } } host := if params.host == '' { 'localhost' } else { params.host } @@ -428,6 +436,11 @@ pub fn run_at[T](global_app &T, params RunParams) ! { for { // Create a new app object for each connection, copy global data like db connections mut request_app := &T{} + $if T is MiddlewareInterface { + request_app = &T{ + middlewares: global_app.middlewares.clone() + } + } $if T is DbInterface { request_app.db = global_app.db } $else { @@ -550,21 +563,45 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) { // should be called first. if !route.path.contains('/:') && url_words == route_words { // We found a match + $if T is MiddlewareInterface { + if validate_middleware(mut app, url.path) == false { + return + } + } + if req.method == .post && method.args.len > 0 { // Populate method args with form values mut args := []string{cap: method.args.len} for param in method.args { args << form[param.name] } - app.$method(args) + + if route.middleware == '' { + app.$method(args) + } else if validate_app_middleware(mut app, route.middleware, method.name) { + app.$method(args) + } } else { - app.$method() + if route.middleware == '' { + app.$method() + } else if validate_app_middleware(mut app, route.middleware, method.name) { + app.$method() + } } return } if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { - app.$method() + $if T is MiddlewareInterface { + if validate_middleware(mut app, url.path) == false { + return + } + } + if route.middleware == '' { + app.$method() + } else if validate_app_middleware(mut app, route.middleware, method.name) { + app.$method() + } return } @@ -573,7 +610,17 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) { if method_args.len != method.args.len { eprintln('warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})') } - app.$method(method_args) + + $if T is MiddlewareInterface { + if validate_middleware(mut app, url.path) == false { + return + } + } + if route.middleware == '' { + app.$method(method_args) + } else if validate_app_middleware(mut app, route.middleware, method.name) { + app.$method(method_args) + } return } } @@ -583,6 +630,49 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) { conn.write(vweb.http_404.bytes()) or {} } +// validate_middleware validates and fires all middlewares that are defined in the global app instance +fn validate_middleware[T](mut app T, full_path string) bool { + for path, middleware_chain in app.middlewares { + // only execute middleware if route.path starts with `path` + if full_path.len >= path.len && full_path.starts_with(path) { + // there is middleware for this route + for func in middleware_chain { + if func(mut app.Context) == false { + return false + } + } + } + } + // passed all middleware checks + return true +} + +// validate_app_middleware validates all middlewares as a method of `app` +fn validate_app_middleware[T](mut app T, middleware string, method_name string) bool { + // then the middleware that is defined for this route specifically + valid := fire_app_middleware(mut app, middleware) or { + eprintln('warning: middleware `${middleware}` for the `${method_name}` are not found') + true + } + return valid +} + +// fire_app_middleware fires all middlewares that are defined as a method of `app` +fn fire_app_middleware[T](mut app T, method_name string) ?bool { + $for method in T.methods { + if method_name == method.name { + $if method.return_type is bool { + return app.$method() + } $else { + eprintln('error in `${method.name}, middleware functions must return bool') + return none + } + } + } + // no middleware function found + return none +} + fn route_matches(url_words []string, route_words []string) ?[]string { // URL path should be at least as long as the route path // except for the catchall route (`/:path...`)