diff --git a/vlib/net/urllib/urllib.v b/vlib/net/urllib/urllib.v index eda6c8c2b5..536516fabf 100644 --- a/vlib/net/urllib/urllib.v +++ b/vlib/net/urllib/urllib.v @@ -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(`:`) diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index 7c7ac7835c..493a7aa037 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -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. diff --git a/vlib/vweb/tests/controller_test.v b/vlib/vweb/tests/controller_test.v index 5fc20d807f..7b83c6f616 100644 --- a/vlib/vweb/tests/controller_test.v +++ b/vlib/vweb/tests/controller_test.v @@ -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') } } diff --git a/vlib/vweb/tests/vweb_test.v b/vlib/vweb/tests/vweb_test.v index a00c51d777..2417e1b5c4 100644 --- a/vlib/vweb/tests/vweb_test.v +++ b/vlib/vweb/tests/vweb_test.v @@ -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() == '' diff --git a/vlib/vweb/tests/vweb_test_server.v b/vlib/vweb/tests/vweb_test_server.v index ef3a4536fd..86be8022bb 100644 --- a/vlib/vweb/tests/vweb_test_server.v +++ b/vlib/vweb/tests/vweb_test_server.v @@ -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' { diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index ab961fae3b..15f7baa334 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -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)