mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
vweb: middleware implementation (#17730)
This commit is contained in:
parent
713c95fcc8
commit
1fe5aca782
21
examples/vweb/middleware/templates/base.html
Normal file
21
examples/vweb/middleware/templates/base.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@title</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/admin/secrets">"/admin/secrets"</a>
|
||||
<a href="/admin/dynamic">"/admin/dynamic"</a>
|
||||
<a href="/early">Early exit</a>
|
||||
</nav>
|
||||
<main>
|
||||
@content
|
||||
</main>
|
||||
<footer></footer>
|
||||
</body>
|
||||
</html>
|
1
examples/vweb/middleware/templates/early.html
Normal file
1
examples/vweb/middleware/templates/early.html
Normal file
@ -0,0 +1 @@
|
||||
<p>Early exit</p>
|
3
examples/vweb/middleware/templates/index.html
Normal file
3
examples/vweb/middleware/templates/index.html
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
<h1>Hello </h1>
|
1
examples/vweb/middleware/templates/secret.html
Normal file
1
examples/vweb/middleware/templates/secret.html
Normal file
@ -0,0 +1 @@
|
||||
<p>Super secret stuff</p>
|
109
examples/vweb/middleware/vweb_example.v
Normal file
109
examples/vweb/middleware/vweb_example.v
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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 {
|
||||
|
350
vlib/vweb/tests/middleware_test.v
Normal file
350
vlib/vweb/tests/middleware_test.v
Normal file
@ -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()
|
||||
}
|
274
vlib/vweb/tests/middleware_test_server.v
Normal file
274
vlib/vweb/tests/middleware_test_server.v
Normal file
@ -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)
|
||||
}
|
@ -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')
|
||||
|
104
vlib/vweb/vweb.v
104
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...`)
|
||||
|
Loading…
x
Reference in New Issue
Block a user