1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vweb: add host option to controller (#18303)

This commit is contained in:
Casper Kuethe 2023-05-30 14:22:23 +02:00 committed by GitHub
parent 05b832a317
commit 4174048f96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 7 deletions

View File

@ -1003,7 +1003,7 @@ pub fn (u &URL) port() string {
// split_host_port separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Per RFC 3986, it requires ports to be numeric.
fn split_host_port(hostport string) (string, string) {
pub fn split_host_port(hostport string) (string, string) {
mut host := hostport
mut port := ''
colon := host.last_index_u8(`:`)

View File

@ -253,7 +253,8 @@ pub fn (mut app App) controller_get_user_by_id() vweb.Result {
```
#### - Host
To restrict an endpoint to a specific host, you can use the `host` attribute
followed by a colon `:` and the host name.
followed by a colon `:` and the host name. You can test the Host feature locally
by adding a host to the "hosts" file of your device.
**Example:**
@ -267,8 +268,17 @@ pub fn (mut app App) hello_web() vweb.Result {
pub fn (mut app App) hello_api() vweb.Result {
return app.text('Hello API')
}
// define the handler without a host attribute last if you have conflicting paths.
['/']
pub fn (mut app App) hello_others() vweb.Result {
return app.text('Hello Others')
}
```
You can also [create a controller](#hosts) to handle all requests from a specific
host in one app.
### Middleware
Vweb has different kinds of middleware.
@ -679,6 +689,43 @@ pub fn (mut app App) admin_path vweb.Result {
There will be an error, because the controller `Admin` handles all routes starting with
`"/admin"`; the method `admin_path` is unreachable.
#### Hosts
You can also set a host for a controller. All requests coming from that host will be handled
by the controller.
**Example:**
```v
module main
import vweb
struct App {
vweb.Context
vweb.Controller
}
pub fn (mut app App) index() vweb.Result {
return app.text('App')
}
struct Example {
vweb.Context
}
// You can only access this route at example.com: http://example.com/
pub fn (mut app Example) index() vweb.Result {
return app.text('Example')
}
fn main() {
vweb.run(&App{
controllers: [
vweb.controller_host('example.com', '/', &Example{}),
]
}, 8080)
}
```
#### Databases and `[vweb_global]` in controllers
Fields with `[vweb_global]` have to passed to each controller individually.

View File

@ -137,9 +137,9 @@ fn test_duplicate_route() {
$if windows {
task := spawn os.execute(server_exec_cmd)
res := task.wait()
assert res.output.contains('V panic: method "duplicate" with route "/admin/duplicate" should be handled by the Controller of "/admin"')
assert res.output.contains('V panic: conflicting paths')
} $else {
res := os.execute(server_exec_cmd)
assert res.output.contains('V panic: method "duplicate" with route "/admin/duplicate" should be handled by the Controller of "/admin"')
assert res.output.contains('V panic: conflicting paths')
}
}

View File

@ -238,6 +238,20 @@ fn test_http_client_multipart_form_data() {
assert x.body == files[0].data
}
fn test_host() {
mut req := http.Request{
url: 'http://${localserver}/with_host'
method: .get
}
mut x := req.do()!
assert x.status() == .not_found
req.add_header(.host, 'example.com')
x = req.do()!
assert x.status() == .ok
}
fn test_http_client_shutdown_does_not_work_without_a_cookie() {
x := http.get('http://${localserver}/shutdown') or {
assert err.msg() == ''

View File

@ -119,6 +119,12 @@ pub fn (mut app App) not_found() vweb.Result {
return app.html('404 on "${app.req.url}"')
}
[host: 'example.com']
['/with_host']
pub fn (mut app App) with_host() vweb.Result {
return app.ok('')
}
pub fn (mut app App) shutdown() vweb.Result {
session_key := app.get_cookie('skey') or { return app.not_found() }
if session_key != 'superman' {

View File

@ -417,8 +417,11 @@ fn generate_routes[T](app &T) !map[string]Route {
type ControllerHandler = fn (ctx Context, mut url urllib.URL, host string, tid int)
pub struct ControllerPath {
pub:
path string
handler ControllerHandler
pub mut:
host string
}
interface ControllerInterface {
@ -448,6 +451,13 @@ pub fn controller[T](path string, global_app &T) &ControllerPath {
}
}
// controller_host generates a controller which only handles incoming requests from the `host` domain
pub fn controller_host[T](host string, path string, global_app &T) &ControllerPath {
mut ctrl := controller(path, global_app)
ctrl.host = host
return ctrl
}
// 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()) }
@ -487,12 +497,14 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
$if T is ControllerInterface {
mut paths := []string{}
for controller in global_app.controllers {
paths << controller.path
if controller.host == '' {
paths << controller.path
}
}
for method_name, route in routes {
for controller_path in paths {
if route.path.starts_with(controller_path) {
return error('method "${method_name}" with route "${route.path}" should be handled by the Controller of "${controller_path}"')
return error('conflicting paths: method "${method_name}" with route "${route.path}" should be handled by the Controller of path "${controller_path}"')
}
}
}
@ -622,7 +634,9 @@ fn handle_conn[T](mut conn net.TcpConn, global_app &T, routes &map[string]Route,
return
}
host := req.header.get(http.CommonHeader.host) or { '' }.to_lower()
// remove the port from the HTTP Host header
host_with_port := req.header.get(.host) or { '' }
host, _ := urllib.split_host_port(host_with_port)
// Create Context with request data
ctx := Context{
@ -637,6 +651,10 @@ fn handle_conn[T](mut conn net.TcpConn, global_app &T, routes &map[string]Route,
// match controller paths
$if T is ControllerInterface {
for controller in global_app.controllers {
// skip controller if the hosts don't match
if controller.host != '' && host != controller.host {
continue
}
if url.path.len >= controller.path.len && url.path.starts_with(controller.path) {
// pass route handling to the controller
controller.handler(ctx, mut url, host, tid)