x.vweb: remove the entire module (it's now veb)

This commit is contained in:
Alexander Medvednikov 2025-01-19 05:42:52 +03:00
parent 504ec54be1
commit e5f70278ea
43 changed files with 30 additions and 6502 deletions

View file

@ -274,8 +274,6 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
if !os.exists('/usr/local/include/wkhtmltox/pdf.h') {
skip_files << 'examples/c_interop_wkhtmltopdf.v' // needs installation of wkhtmltopdf from https://github.com/wkhtmltopdf/packaging/releases
}
skip_files << 'vlib/vweb/vweb_app_test.v' // imports the `sqlite` module, which in turn includes sqlite3.h
skip_files << 'vlib/x/vweb/tests/vweb_app_test.v' // imports the `sqlite` module, which in turn includes sqlite3.h
skip_files << 'vlib/veb/tests/veb_app_test.v' // imports the `sqlite` module, which in turn includes sqlite3.h
}
$if !macos {

View file

@ -299,8 +299,6 @@ const skip_on_ubuntu_musl = [
'vlib/v/tests/websocket_logger_interface_should_compile_test.v',
'vlib/v/tests/fns/fn_literal_type_test.v',
'vlib/x/sessions/tests/db_store_test.v',
'vlib/x/vweb/tests/vweb_test.v',
'vlib/x/vweb/tests/vweb_app_test.v',
'vlib/veb/tests/veb_app_test.v',
]
const skip_on_linux = [

View file

@ -2767,6 +2767,8 @@ fn (mut c Checker) hash_stmt(mut node ast.HashStmt) {
fn (mut c Checker) import_stmt(node ast.Import) {
if node.mod == 'x.vweb' {
println('`x.vweb` is now `veb`. The module is no longer experimental. Simply `import veb` instead of `import x.vweb`.')
} else if node.mod == 'vweb' {
println('`vweb` has been deprecated. Please use the more stable and fast `veb`has been deprecated. Please use the more stable and fast `veb`` instead of `import x.vweb`.')
}
c.check_valid_snake_case(node.alias, 'module alias', node.pos)
for sym in node.syms {

View file

@ -8,7 +8,7 @@ The sessions module provides an implementation for [session stores](#custom-stor
The session store handles the saving, storing and retrieving of data. You can
either use a store directly yourself, or you can use the `session.Sessions` struct
which is easier to use since it also handles session verification and integrates nicely
with vweb.
with veb.
If you want to use `session.Sessions` in your web app the session id's will be
stored using cookies. The best way to get started is to follow the
@ -18,10 +18,10 @@ Otherwise have a look at the [advanced usage](#advanced-usage) section.
## Getting Started
The examples in this section use `x.vweb`. See the [advanced usage](#advanced-usage) section
for examples without `x.vweb`.
The examples in this section use `x.veb`. See the [advanced usage](#advanced-usage) section
for examples without `x.veb`.
To start using sessions in vweb embed `sessions.CurrentSession` on the
To start using sessions in veb embed `sessions.CurrentSession` on the
Context struct and add `sessions.Sessions` to the app struct. We must also pass the type
of our session data.
@ -30,7 +30,7 @@ For any further example code we will use the `User` struct.
```v ignore
import x.sessions
import x.vweb
import veb
pub struct User {
pub mut:
@ -39,7 +39,7 @@ pub mut:
}
pub struct Context {
vweb.Context
veb.Context
// By embedding the CurrentSession struct we can directly access the current session id
// and any associated session data. Set the session data type to `User`
sessions.CurrentSession[User]
@ -48,18 +48,18 @@ pub struct Context {
pub struct App {
pub mut:
// this struct contains the store that holds all session data it also provides
// an easy way to manage sessions in your vweb app. Set the session data type to `User`
// an easy way to manage sessions in your veb app. Set the session data type to `User`
sessions &sessions.Sessions[User]
}
```
Next we need to create the `&sessions.Sessions[User]` instance for our app. This
struct provides functionality to easier manage sessions in a vweb app.
struct provides functionality to easier manage sessions in a veb app.
### Session Stores
To create `sessions.Sessions` We must specify a "store" which handles the session data.
Currently vweb provides two options for storing session data:
Currently veb provides two options for storing session data:
1. The `MemoryStore[T]` stores session data in memory only using the `map` datatype.
2. The `DBStore[T]` stores session data in a database by encoding the session data to JSON.
@ -81,13 +81,13 @@ fn main() {
secret: 'my secret'.bytes()
}
vweb.run[App, Context](mut app, 8080)
veb.run[App, Context](mut app, 8080)
}
```
### Middleware
The `sessions.vweb2_middleware` module provides a middleware handler. This handler will execute
The `sessions.veb2_middleware` module provides a middleware handler. This handler will execute
before your own route handlers and will verify the current session and fetch any associated
session data and load it into `sessions.CurrentSession`, which is embedded on the Context struct.
@ -99,14 +99,14 @@ session data and load it into `sessions.CurrentSession`, which is embedded on th
```v ignore
// add this import at the top of your file
import x.sessions.vweb2_middleware
import x.sessions.veb2_middleware
pub struct App {
// embed the Middleware struct from vweb
vweb.Middleware[Context]
// embed the Middleware struct from veb
veb.Middleware[Context]
pub mut:
// this struct contains the store that holds all session data it also provides
// an easy way to manage sessions in your vweb app. Set the session data type to `User`
// an easy way to manage sessions in your veb app. Set the session data type to `User`
sessions &sessions.Sessions[User]
}
@ -118,13 +118,13 @@ fn main() {
}
// register the sessions middleware
app.use(vweb2_middleware.create[User, Context](mut app.sessions))
app.use(veb2_middleware.create[User, Context](mut app.sessions))
vweb.run[App, Context](mut app, 8080)
veb.run[App, Context](mut app, 8080)
}
```
You can now start using sessions with vweb!
You can now start using sessions with veb!
### Usage in endpoint handlers
@ -137,7 +137,7 @@ if no data is set.
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
// check if a user is logged in
if user := ctx.session_data {
return ctx.text('Welcome ${user.name}! Verification status: ${user.verified}')
@ -160,7 +160,7 @@ method. This method will save the data and *always* set a new session id.
**Example:**
```v ignore
pub fn (mut app App) login(mut ctx Context) vweb.Result {
pub fn (mut app App) login(mut ctx Context) veb.Result {
// set a session id cookie and save data for the new user
app.sessions.save(mut ctx, User{
name: '[no name provided]'
@ -179,7 +179,7 @@ query parameter is not passed an error 400 (bad request) is returned.
**Example:**
```v ignore
pub fn (mut app App) save(mut ctx Context) vweb.Result {
pub fn (mut app App) save(mut ctx Context) veb.Result {
// check if there is a session
app.sessions.get(ctx) or { return ctx.request_error('You are not logged in :(') }
@ -205,7 +205,7 @@ method.
**Example:**
```v ignore
pub fn (mut app App) logout(mut ctx Context) vweb.Result {
pub fn (mut app App) logout(mut ctx Context) veb.Result {
app.sessions.logout(mut ctx) or { return ctx.server_error('could not logout, please try again') }
return ctx.text('You are now logged out!')
}
@ -249,7 +249,7 @@ in, you can set a new session id with `resave`..
## Advanced Usage
If you want to store session id's in another manner than cookies, or if you want
to use this sessions module outside of vweb, the easiest way is to create an
to use this sessions module outside of veb, the easiest way is to create an
instance of a `Store` and directly interact with it.
First we create an instance of the `MemoryStore` and pass the user struct as data type.

View file

@ -62,7 +62,7 @@ HTML tags are always escaped in text file : @html_section
### 2. Minimal Vweb example:
```v
import x.vweb
import x.veb
import x.templating.dtm
import os
@ -72,7 +72,7 @@ pub mut:
}
pub struct Context {
vweb.Context
veb.Context
}
fn main() {
@ -96,11 +96,11 @@ fn main() {
)
*/
vweb.run[App, Context](mut app, 18081)
veb.run[App, Context](mut app, 18081)
}
@['/']
pub fn (mut app App) index(mut ctx Context) vweb.Result {
pub fn (mut app App) index(mut ctx Context) veb.Result {
mut tmpl_var := map[string]dtm.DtmMultiTypeMap{}
tmpl_var['title'] = 'The true title'
html_content := app.dtmi.expand('index.html', placeholders: &tmpl_var)
@ -125,7 +125,7 @@ pub fn (mut app App) index(mut ctx Context) vweb.Result {
```
You have a ready-to-view demonstration available
[here](https://github.com/vlang/v/tree/master/vlib/vweb/tests/dynamic_template_manager_test_server).
[here](https://github.com/vlang/v/tree/master/vlib/veb/tests/dynamic_template_manager_test_server).
## Available Options

View file

@ -1,916 +0,0 @@
# vweb - the V Web Server
A simple yet powerful web server with built-in routing, parameter handling, templating, and other
features.
## Features
- **Very fast** performance of C on the web.
- **Templates are precompiled** all errors are visible at compilation time, not at runtime.
- **Middleware** functionality similar to other big frameworks.
- **Controllers** to split up your apps logic.
- **Easy to deploy** just one binary file that also includes all templates. No need to install any
dependencies.
## Quick Start
Run your vweb app with a live reload via `v -d vweb_livereload watch run .`
Now modifying any file in your web app (whether it's a .v file with the backend logic
or a compiled .html template file) will result in an instant refresh of your app
in the browser. No need to quit the app, rebuild it, and refresh the page in the browser!
## Deploying vweb apps
All the code, including HTML templates, is in one binary file. That's all you need to deploy.
Use the `-prod` flag when building for production.
## Getting Started
To start, you must import the module `x.vweb` and define a structure which will
represent your app and a structure which will represent the context of a request.
These structures must be declared with the `pub` keyword.
**Example:**
```v
module main
import x.vweb
pub struct User {
pub mut:
name string
id int
}
// Our context struct must embed `vweb.Context`!
pub struct Context {
vweb.Context
pub mut:
// In the context struct we store data that could be different
// for each request. Like a User struct or a session id
user User
session_id string
}
pub struct App {
pub:
// In the app struct we store data that should be accessible by all endpoints.
// For example, a database or configuration values.
secret_key string
}
// This is how endpoints are defined in vweb. This is the index route
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('Hello V! The secret key is "${app.secret_key}"')
}
fn main() {
mut app := &App{
secret_key: 'secret'
}
// Pass the App and context type and start the web server on port 8080
vweb.run[App, Context](mut app, 8080)
}
```
You can use the `App` struct for data you want to keep during the lifetime of your program,
or for data that you want to share between different routes.
A new `Context` struct is created every time a request is received,
so it can contain different data for each request.
## Defining endpoints
To add endpoints to your web server, you must extend the `App` struct.
For routing you can either use auto-mapping of function names or specify the path as an attribute.
The function expects a parameter of your Context type and a response of the type `vweb.Result`.
**Example:**
```v ignore
// This endpoint can be accessed via http://server:port/hello
pub fn (app &App) hello(mut ctx Context) vweb.Result {
return ctx.text('Hello')
}
// This endpoint can be accessed via http://server:port/foo
@['/foo']
pub fn (app &App) world(mut ctx Context) vweb.Result {
return ctx.text('World')
}
```
### HTTP verbs
To use any HTTP verbs (or methods, as they are properly called),
such as `@[post]`, `@[get]`, `@[put]`, `@[patch]` or `@[delete]`
you can simply add the attribute before the function definition.
**Example:**
```v ignore
// only GET requests to http://server:port/world are handled by this method
@[get]
pub fn (app &App) world(mut ctx Context) vweb.Result {
return ctx.text('World')
}
// only POST requests to http://server:port/product/create are handled by this method
@['/product/create'; post]
pub fn (app &App) create_product(mut ctx Context) vweb.Result {
return ctx.text('product')
}
```
By default, endpoints are marked as GET requests only. It is also possible to
add multiple HTTP verbs per endpoint.
**Example:**
```v ignore
// only GET and POST requests to http://server:port/login are handled by this method
@['/login'; get; post]
pub fn (app &App) login(mut ctx Context) vweb.Result {
if ctx.req.method == .get {
// show the login page on a GET request
return ctx.html('<h1>Login page</h1><p>todo: make form</p>')
} else {
// request method is POST
password := ctx.form['password']
// validate password length
if password.len < 12 {
return ctx.text('password is too weak!')
} else {
// we receive a POST request, so we want to explicitly tell the browser
// to send a GET request to the profile page.
return ctx.redirect('/profile')
}
}
}
```
### Routes with Parameters
Parameters are passed directly to an endpoint route using the colon sign `:`. The route
parameters are passed as arguments. V will cast the parameter to any of V's primitive types
(`string`, `int` etc,).
To pass a parameter to an endpoint, you simply define it inside an attribute, e. g.
`@['/hello/:user]`.
After it is defined in the attribute, you have to add it as a function parameter.
**Example:**
```v ignore
// V will pass the parameter 'user' as a string
vvvv
@['/hello/:user'] vvvv
pub fn (app &App) hello_user(mut ctx Context, user string) vweb.Result {
return ctx.text('Hello ${user}')
}
// V will pass the parameter 'id' as an int
vv
@['/document/:id'] vv
pub fn (app &App) get_document(mut ctx Context, id int) vweb.Result {
return ctx.text('Hello ${user}')
}
```
If we visit http://localhost:port/hello/vaesel we would see the text `Hello vaesel`.
### Routes with Parameter Arrays
If you want multiple parameters in your route and if you want to parse the parameters
yourself, or you want a wildcard route, you can add `...` after the `:` and name,
e.g. `@['/:path...']`.
This will match all routes after `'/'`. For example, the url `/path/to/test` would give
`path = '/path/to/test'`.
```v ignore
vvv
@['/:path...'] vvvv
pub fn (app &App) wildcard(mut ctx Context, path string) vweb.Result {
return ctx.text('URL path = "${path}"')
}
```
### Query, Form and Files
You have direct access to query values by accessing the `query` field on your context struct.
You are also able to access any formdata or files that were sent
with the request with the fields `.form` and `.files` respectively.
In the following example, visiting http://localhost:port/user?name=vweb we
will see the text `Hello vweb!`. And if we access the route without the `name` parameter,
http://localhost:port/user, we will see the text `no user was found`,
**Example:**
```v ignore
@['/user'; get]
pub fn (app &App) get_user_by_id(mut ctx Context) vweb.Result {
user_name := ctx.query['name'] or {
// we can exit early and send a different response if no `name` parameter was passed
return ctx.text('no user was found')
}
return ctx.text('Hello ${user_name}!')
}
```
### Host
To restrict an endpoint to a specific host, you can use the `host` attribute
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:**
```v ignore
@['/'; host: 'example.com']
pub fn (app &App) hello_web(mut ctx Context) vweb.Result {
return app.text('Hello World')
}
@['/'; host: 'api.example.org']
pub fn (app &App) hello_api(mut ctx Context) vweb.Result {
return ctx.text('Hello API')
}
// define the handler without a host attribute last if you have conflicting paths.
@['/']
pub fn (app &App) hello_others(mut ctx Context) vweb.Result {
return ctx.text('Hello Others')
}
```
You can also [create a controller](#controller-with-hostname) to handle all requests from a specific
host in one app struct.
### Route Matching Order
vweb will match routes in the order that you define endpoints.
**Example:**
```v ignore
@['/:path']
pub fn (app &App) with_parameter(mut ctx Context, path string) vweb.Result {
return ctx.text('from with_parameter, path: "${path}"')
}
@['/normal']
pub fn (app &App) normal(mut ctx Context) vweb.Result {
return ctx.text('from normal')
}
```
In this example we defined an endpoint with a parameter first. If we access our app
on the url http://localhost:port/normal we will not see `from normal`, but
`from with_parameter, path: "normal"`.
### Custom not found page
You can implement a `not_found` endpoint that is called when a request is made, and no
matching route is found to replace the default HTTP 404 not found page. This route
has to be defined on our Context struct.
**Example:**
```v ignore
pub fn (mut ctx Context) not_found() vweb.Result {
// set HTTP status 404
ctx.res.set_status(.not_found)
return ctx.html('<h1>Page not found!</h1>')
}
```
## Static files and website
vweb also provides a way of handling static files. We can mount a folder at the root
of our web app, or at a custom route. To start using static files we have to embed
`vweb.StaticHandler` on our app struct.
**Example:**
Let's say you have the following file structure:
```
.
├── static/
│ ├── css/
│ │ └── main.css
│ └── js/
│ └── main.js
└── main.v
```
If we want all the documents inside the `static` sub-directory to be publicly accessible, we can
use `handle_static`.
> **Note:**
> vweb will recursively search the folder you mount; all the files inside that folder
> will be publicly available.
_main.v_
```v
module main
import x.vweb
pub struct Context {
vweb.Context
}
pub struct App {
vweb.StaticHandler
}
fn main() {
mut app := &App{}
app.handle_static('static', false)!
vweb.run[App, Context](mut app, 8080)
}
```
If we start the app with `v run main.v` we can access our `main.css` file at
http://localhost:8080/static/css/main.css
### Mounting folders at specific locations
In the previous example the folder `static` was mounted at `/static`. We could also choose
to mount the static folder at the root of our app: everything inside the `static` folder
is available at `/`.
**Example:**
```v ignore
// change the second argument to `true` to mount a folder at the app root
app.handle_static('static', true)!
```
We can now access `main.css` directly at http://localhost:8080/css/main.css.
If a request is made to the root of a static folder, vweb will look for an
`index.html` or `ìndex.htm` file and serve it if available.
Thus, it's also a good way to host a complete website.
An example is available [here](/examples/vweb/static_website).
It is also possible to mount the `static` folder at a custom path.
**Example:**
```v ignore
// mount the folder 'static' at path '/public', the path has to start with '/'
app.mount_static_folder_at('static', '/public')
```
If we run our app the `main.css` file is available at http://localhost:8080/public/main.css
### Adding a single static asset
If you don't want to mount an entire folder, but only a single file, you can use `serve_static`.
**Example:**
```v ignore
// serve the `main.css` file at '/path/main.css'
app.serve_static('/path/main.css', 'static/css/main.css')!
```
### Dealing with MIME types
By default, vweb will map the extension of a file to a MIME type. If any of your static file's
extensions do not have a default MIME type in vweb, vweb will throw an error and you
have to add your MIME type to `.static_mime_types` yourself.
**Example:**
Let's say you have the following file structure:
```
.
├── static/
│ └── file.what
└── main.v
```
```v ignore
app.handle_static('static', true)!
```
This code will throw an error, because vweb has no default MIME type for a `.what` file extension.
```
unknown MIME type for file extension ".what"
```
To fix this we have to provide a MIME type for the `.what` file extension:
```v ignore
app.static_mime_types['.what'] = 'txt/plain'
app.handle_static('static', true)!
```
## Middleware
Middleware in web development is (loosely defined) a hidden layer that sits between
what a user requests (the HTTP Request) and what a user sees (the HTTP Response).
We can use this middleware layer to provide "hidden" functionality to our apps endpoints.
To use vweb's middleware we have to embed `vweb.Middleware` on our app struct and provide
the type of which context struct should be used.
**Example:**
```v ignore
pub struct App {
vweb.Middleware[Context]
}
```
### Use case
We could, for example, get the cookies for an HTTP request and check if the user has already
accepted our cookie policy. Let's modify our Context struct to store whether the user has
accepted our policy or not.
**Example:**
```v ignore
pub struct Context {
vweb.Context
pub mut:
has_accepted_cookies bool
}
```
In vweb middleware functions take a `mut` parameter with the type of your context struct
and must return `bool`. We have full access to modify our Context struct!
The return value indicates to vweb whether it can continue or has to stop. If we send a
response to the client in a middleware function vweb has to stop, so we return `false`.
**Example:**
```v ignore
pub fn check_cookie_policy(mut ctx Context) bool {
// get the cookie
cookie_value := ctx.get_cookie('accepted_cookies') or { '' }
// check if the cookie has been set
if cookie_value == 'true' {
ctx.has_accepted_cookies = true
}
// we don't send a response, so we must return true
return true
}
```
We can check this value in an endpoint and return a different response.
**Example:**
```v ignore
@['/only-cookies']
pub fn (app &App) only_cookie_route(mut ctx Context) vweb.Result {
if ctx.has_accepted_cookies {
return ctx.text('Welcome!')
} else {
return ctx.text('You must accept the cookie policy!')
}
}
```
There is one thing left for our middleware to work: we have to register our `only_cookie_route`
function as middleware for our app. We must do this after the app is created and before the
app is started.
**Example:**
```v ignore
fn main() {
mut app := &App{}
// register middleware for all routes
app.use(handler: only_cookie_route)
// Pass the App and context type and start the web server on port 8080
vweb.run[App, Context](mut app, 8080)
}
```
### Types of middleware
In the previous example we used so called "global" middleware. This type of middleware
applies to every endpoint defined on our app struct; global. It is also possible
to register middleware for only a certain route(s).
**Example:**
```v ignore
// register middleware only for the route '/auth'
app.route_use('/auth', handler: auth_middleware)
// register middleware only for the route '/documents/' with a parameter
// e.g. '/documents/5'
app.route_use('/documents/:id')
// register middleware with a parameter array. The middleware will be registered
// for all routes that start with '/user/' e.g. '/user/profile/update'
app.route_use('/user/:path...')
```
### Evaluation moment
By default, the registered middleware functions are executed *before* a method on your
app struct is called. You can also change this behaviour to execute middleware functions
*after* a method on your app struct is called, but before the response is sent!
**Example:**
```v ignore
pub fn modify_headers(mut ctx Context) bool {
// add Content-Language: 'en-US' header to each response
ctx.res.header.add(.content_language, 'en-US')
return true
}
```
```v ignore
app.use(handler: modify_headers, after: true)
```
#### When to use which type
You could use "before" middleware to check and modify the HTTP request and you could use
"after" middleware to validate the HTTP response that will be sent or do some cleanup.
Anything you can do in "before" middleware, you can do in "after" middleware.
### Evaluation order
vweb will handle requests in the following order:
1. Execute global "before" middleware
2. Execute "before" middleware that matches the requested route
3. Execute the endpoint handler on your app struct
4. Execute global "after" middleware
5. Execute "after" middleware that matches the requested route
In each step, except for step `3`, vweb will evaluate the middleware in the order that
they are registered; when you call `app.use` or `app.route_use`.
### Early exit
If any middleware sends a response (and thus must return `false`) vweb will not execute any
other middleware, or the endpoint method, and immediately send the response.
**Example:**
```v ignore
pub fn early_exit(mut ctx Context) bool {
ctx.text('early exit')
// we send a response from middleware, so we have to return false
return false
}
pub fn logger(mut ctx Context) bool {
println('received request for "${ctx.req.url}"')
return true
}
```
```v ignore
app.use(handler: early_exit)
app.use(handler: logger)
```
Because we register `early_exit` before `logger` our logging middleware will never be executed!
## Controllers
Controllers can be used to split up your app logic so you are able to have one struct
per "route group". E.g. a struct `Admin` for urls starting with `'/admin'` and a struct `Foo`
for urls starting with `'/foo'`.
To use controllers we have to embed `vweb.Controller` on
our app struct and when we register a controller we also have to specify
what the type of the context struct will be. That means that it is possible
to have a different context struct for each controller and the main app struct.
**Example:**
```v
module main
import x.vweb
pub struct Context {
vweb.Context
}
pub struct App {
vweb.Controller
}
// this endpoint will be available at '/'
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('from app')
}
pub struct Admin {}
// this endpoint will be available at '/admin/'
pub fn (app &Admin) index(mut ctx Context) vweb.Result {
return ctx.text('from admin')
}
pub struct Foo {}
// this endpoint will be available at '/foo/'
pub fn (app &Foo) index(mut ctx Context) vweb.Result {
return ctx.text('from foo')
}
fn main() {
mut app := &App{}
// register the controllers the same way as how we start a vweb app
mut admin_app := &Admin{}
app.register_controller[Admin, Context]('/admin', mut admin_app)!
mut foo_app := &Foo{}
app.register_controller[Foo, Context]('/foo', mut foo_app)!
vweb.run[App, Context](mut app, 8080)
}
```
You can do everything with a controller struct as with a regular `App` struct.
Register middleware, add static files and you can even register other controllers!
### Routing
Any route inside a controller struct is treated as a relative route to its controller namespace.
```v ignore
@['/path']
pub fn (app &Admin) path(mut ctx Context) vweb.Result {
return ctx.text('Admin')
}
```
When we registered the controller with
`app.register_controller[Admin, Context]('/admin', mut admin_app)!`
we told vweb that the namespace of that controller is `'/admin'` so in this example we would
see the text "Admin" if we navigate to the url `'/admin/path'`.
vweb doesn't support duplicate routes, so if we add the following
route to the example the code will produce an error.
```v ignore
@['/admin/path']
pub fn (app &App) admin_path(mut ctx Context) vweb.Result {
return ctx.text('Admin overwrite')
}
```
There will be an error, because the controller `Admin` handles all routes starting with
`'/admin'`: the endpoint `admin_path` is unreachable.
### Controller with hostname
You can also set a host for a controller. All requests coming to that host will be handled
by the controller.
**Example:**
```v ignore
struct Example {}
// You can only access this route at example.com: http://example.com/
pub fn (app &Example) index(mut ctx Context) vweb.Result {
return ctx.text('Example')
}
```
```v ignore
mut example_app := &Example{}
// set the controllers hostname to 'example.com' and handle all routes starting with '/',
// we handle requests with any route to 'example.com'
app.register_controller[Example, Context]('example.com', '/', mut example_app)!
```
## Context Methods
vweb has a number of utility methods that make it easier to handle requests and send responses.
These methods are available on `vweb.Context` and directly on your own context struct if you
embed `vweb.Context`. Below are some of the most used methods, look at the
[standard library documentation](https://modules.vlang.io/) to see them all.
### Request methods
You can directly access the HTTP request on the `.req` field.
#### Get request headers
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
content_length := ctx.get_header(.content_length) or { '0' }
// get custom header
custom_header := ctx.get_custom_header('X-HEADER') or { '' }
// ...
}
```
#### Get a cookie
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
cookie_val := ctx.get_cookie('token') or { '' }
// ...
}
```
### Response methods
You can directly modify the HTTP response by changing the `res` field,
which is of the type `http.Response`.
#### Send response with different MIME types
```v ignore
// send response HTTP_OK with content-type `text/html`
ctx.html('<h1>Hello world!</h1>')
// send response HTTP_OK with content-type `text/plain`
ctx.text('Hello world!')
// stringify the object and send response HTTP_OK with content-type `application/json`
ctx.json(User{
name: 'test'
age: 20
})
```
#### Sending files
**Example:**
```v ignore
pub fn (app &App) file_response(mut ctx Context) vweb.Result {
// send the file 'image.png' in folder 'data' to the user
return ctx.file('data/image.png')
}
```
#### Set response headers
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
ctx.set_header(.accept, 'text/html')
// set custom header
ctx.set_custom_header('X-HEADER', 'my-value')!
// ...
}
```
#### Set a cookie
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
ctx.set_cookie(http.Cookie{
name: 'token'
value: 'true'
path: '/'
secure: true
http_only: true
})
// ...
}
```
#### Redirect
You must pass the type of redirect to vweb:
- `moved_permanently` HTTP code 301
- `found` HTTP code 302
- `see_other` HTTP code 303
- `temporary_redirect` HTTP code 307
- `permanent_redirect` HTTP code 308
**Common use cases:**
If you want to change the request method, for example when you receive a post request and
want to redirect to another page via a GET request, you should use `see_other`. If you want
the HTTP method to stay the same, you should use `found` generally speaking.
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) vweb.Result {
token := ctx.get_cookie('token') or { '' }
if token == '' {
// redirect the user to '/login' if the 'token' cookie is not set
// we explicitly tell the browser to send a GET request
return ctx.redirect('/login', typ: .see_other)
} else {
return ctx.text('Welcome!')
}
}
```
#### Sending error responses
**Example:**
```v ignore
pub fn (app &App) login(mut ctx Context) vweb.Result {
if username := ctx.form['username'] {
return ctx.text('Hello "${username}"')
} else {
// send an HTTP 400 Bad Request response with a message
return ctx.request_error('missing form value "username"')
}
}
```
You can also use `ctx.server_error(msg string)` to send an HTTP 500 internal server
error with a message.
## Advanced usage
If you need more control over the TCP connection with a client, for example when
you want to keep the connection open. You can call `ctx.takeover_conn`.
When this function is called you are free to do anything you want with the TCP
connection and vweb will not interfere. This means that we are responsible for
sending a response over the connection and closing it.
### Empty Result
Sometimes you want to send the response in another thread, for example when using
[Server Sent Events](sse/README.md). When you are sure that a response will be sent
over the TCP connection you can return `vweb.no_result()`. This function does nothing
and returns an empty `vweb.Result` struct, letting vweb know that we sent a response ourselves.
> **Note:**
> It is important to call `ctx.takeover_conn` before you spawn a thread
**Example:**
```v
module main
import net
import time
import x.vweb
pub struct Context {
vweb.Context
}
pub struct App {}
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('hello!')
}
@['/long']
pub fn (app &App) long_response(mut ctx Context) vweb.Result {
// let vweb know that the connection should not be closed
ctx.takeover_conn()
// use spawn to handle the connection in another thread
// if we don't the whole web server will block for 10 seconds,
// since vweb is singlethreaded
spawn handle_connection(mut ctx.conn)
// we will send a custom response ourselves, so we can safely return an empty result
return vweb.no_result()
}
fn handle_connection(mut conn net.TcpConn) {
defer {
conn.close() or {}
}
// block for 10 second
time.sleep(time.second * 10)
conn.write_string('HTTP/1.1 200 OK\r\nContent-type: text/html\r\nContent-length: 15\r\n\r\nHello takeover!') or {}
}
fn main() {
mut app := &App{}
vweb.run[App, Context](mut app, 8080)
}
```

View file

@ -1,177 +0,0 @@
# Assets
The asset manager for vweb. You can use this asset manager to minify CSS and JavaScript files,
combine them into a single file and to make sure the asset you're using exists.
## Usage
Add `AssetManager` to your App struct to use the asset manager.
**Example:**
```v
module main
import x.vweb
import x.vweb.assets
pub struct Context {
vweb.Context
}
pub struct App {
pub mut:
am assets.AssetManager
}
fn main() {
mut app := &App{}
vweb.run[App, Context](mut app, 8080)
}
```
### Including assets
If you want to include an asset in your templates you can use the `include` method.
First pass the type of asset (css or js), then specify the "include name" of an asset.
**Example:**
```html
@{app.am.include(.css, 'main.css')}
```
Will generate
```html
<link rel="stylesheet" href="/main.css" />
```
### Adding assets
To add an asset use the `add` method. You must specify the path of the asset and what its
include name will be: the name that you will use in templates.
**Example:**
```v ignore
// add a css file at the path "css/main.css" and set its include name to "main.css"
app.am.add(.css, 'css/main.css', 'main.css')
```
### Minify and Combine assets
If you want to minify each asset you must set the `minify` field and specify the cache
folder. Each assest you add is minifed and outputted in `cache_dir`.
**Example:**
```v ignore
pub struct App {
pub mut:
am assets.AssetManager = assets.AssetManager{
cache_dir: 'dist'
minify: true
}
}
```
To combine the all currently added assets into a single file you must call the `combine` method
and specify which asset type you want to combine.
**Example:**
```v ignore
// `combine` returns the path of the minified file
minified_file := app.am.combine(.css)!
```
### Handle folders
You can use the asset manger in combination with vweb's `StaticHandler` to serve
assets in a folder as static assets.
**Example:**
```v ignore
pub struct App {
vweb.StaticHandler
pub mut:
am assets.AssetManager
}
```
Let's say we have the following folder structure:
```
assets/
├── css/
│ └── main.css
└── js/
└── main.js
```
We can tell the asset manager to add all assets in the `static` folder
**Example:**
```v ignore
fn main() {
mut app := &App{}
// add all assets in the "assets" folder
app.am.handle_assets('assets')!
// serve all files in the "assets" folder as static files
app.handle_static('assets', false)!
// start the app
vweb.run[App, Context](mut app, 8080)
}
```
The include name of each minified asset will be set to its relative path,
so if you want to include `main.css` in your template you would write
`@{app.am.include('css/main.css')}`
#### Minify
If you add an asset folder and want to minify those assets you can call the
`cleanup_cache` method to remove old files from the cache folder
that are no longer needed.
**Example:**
```v ignore
pub struct App {
vweb.StaticHandler
pub mut:
am assets.AssetManager = assets.AssetManager{
cache_dir: 'dist'
minify: true
}
}
fn main() {
mut app := &App{}
// add all assets in the "assets" folder
app.am.handle_assets('assets')!
// remove all old cached files from the cache folder
app.am.cleanup_cache()!
// serve all files in the "assets" folder as static files
app.handle_static('assets', false)!
// start the app
vweb.run[App, Context](mut app, 8080)
}
```
#### Prefix the include name
You can add a custom prefix to the include name of assets when adding a folder.
**Example:**
```v ignore
// add all assets in the "assets" folder
app.am.handle_assets_at('assets', 'static')!
```
Now if you want to include `main.css` you would write
``@{app.am.include('static/css/main.css')}`

View file

@ -1,311 +0,0 @@
module assets
import crypto.md5
import os
import strings
import time
import x.vweb
pub enum AssetType {
css
js
all
}
pub struct Asset {
pub:
kind AssetType
file_path string
last_modified time.Time
include_name string
}
pub struct AssetManager {
mut:
css []Asset
js []Asset
cached_file_names []string
pub mut:
// when true assets will be minified
minify bool
// the directory to store the cached/combined files
cache_dir string
// how a combined file should be named. For example for css the extension '.css'
// will be added to the end of `combined_file_name`
combined_file_name string = 'combined'
}
fn (mut am AssetManager) add_asset_directory(directory_path string, traversed_path string) ! {
files := os.ls(directory_path)!
if files.len > 0 {
for file in files {
full_path := os.join_path(directory_path, file)
relative_path := os.join_path(traversed_path, file)
if os.is_dir(full_path) {
am.add_asset_directory(full_path, relative_path)!
} else {
ext := os.file_ext(full_path)
match ext {
'.css' { am.add(.css, full_path, relative_path)! }
'.js' { am.add(.js, full_path, relative_path)! }
// ignore non css/js files
else {}
}
}
}
}
}
// handle_assets recursively walks `directory_path` and adds any assets to the asset manager
pub fn (mut am AssetManager) handle_assets(directory_path string) ! {
return am.add_asset_directory(directory_path, '')
}
// handle_assets_at recursively walks `directory_path` and adds any assets to the asset manager.
// The include name of assets are prefixed with `prepend`
pub fn (mut am AssetManager) handle_assets_at(directory_path string, prepend string) ! {
// remove trailing '/'
return am.add_asset_directory(directory_path, prepend.trim_right('/'))
}
// get all assets of type `asset_type`
pub fn (am AssetManager) get_assets(asset_type AssetType) []Asset {
return match asset_type {
.css {
am.css
}
.js {
am.js
}
.all {
mut assets := []Asset{}
assets << am.css
assets << am.js
assets
}
}
}
// add an asset to the asset manager
pub fn (mut am AssetManager) add(asset_type AssetType, file_path string, include_name string) ! {
if asset_type == .all {
return error('cannot minify asset of type "all"')
}
if !os.exists(file_path) {
return error('cnanot add asset: file "${file_path}" does not exist')
}
last_modified_unix := os.file_last_mod_unix(file_path)
mut real_path := file_path
if am.minify {
// minify and cache file if it was modified
output_path, is_cached := am.minify_and_cache(asset_type, real_path, last_modified_unix,
include_name)!
if is_cached == false && am.exists(asset_type, include_name) {
// file was not modified between the last call to `add`
// and the file was already in the asset manager, so we don't need to
// add it again
return
}
real_path = output_path
}
asset := Asset{
kind: asset_type
file_path: real_path
last_modified: time.unix(last_modified_unix)
include_name: include_name
}
match asset_type {
.css { am.css << asset }
.js { am.js << asset }
else {}
}
}
fn (mut am AssetManager) minify_and_cache(asset_type AssetType, file_path string, last_modified i64, include_name string) !(string, bool) {
if asset_type == .all {
return error('cannot minify asset of type "all"')
}
if am.cache_dir == '' {
return error('cannot minify asset: cache directory is not valid')
} else if !os.exists(am.cache_dir) {
os.mkdir_all(am.cache_dir)!
}
cache_key := am.get_cache_key(file_path, last_modified)
output_file := '${cache_key}.${asset_type}'
output_path := os.join_path(am.cache_dir, output_file)
if os.exists(output_path) {
// the output path already exists, this means that the file has
// been minifed and cached before and hasn't changed in the meantime
am.cached_file_names << output_file
return output_path, false
} else {
// check if the file has been minified before, but is modified.
// if that's the case we remove the old cached file
cached_files := os.ls(am.cache_dir)!
hash := cache_key.all_before('-')
for file in cached_files {
if file.starts_with(hash) {
os.rm(os.join_path(am.cache_dir, file))!
}
}
}
txt := os.read_file(file_path)!
minified := match asset_type {
.css { minify_css(txt) }
.js { minify_js(txt) }
else { '' }
}
os.write_file(output_path, minified)!
am.cached_file_names << output_file
return output_path, true
}
fn (mut am AssetManager) get_cache_key(file_path string, last_modified i64) string {
abs_path := if os.is_abs_path(file_path) { file_path } else { os.resource_abs_path(file_path) }
hash := md5.sum(abs_path.bytes())
return '${hash.hex()}-${last_modified}'
}
// cleanup_cache removes all files in the cache directory that aren't cached at the time
// this function is called
pub fn (mut am AssetManager) cleanup_cache() ! {
if am.cache_dir == '' {
return error('[vweb.assets]: cache directory is not valid')
}
cached_files := os.ls(am.cache_dir)!
// loop over all the files in the cache directory. If a file isn't cached, remove it
for file in cached_files {
ext := os.file_ext(file)
if ext !in ['.css', '.js'] || file in am.cached_file_names {
continue
} else if !file.starts_with(am.combined_file_name) {
os.rm(os.join_path(am.cache_dir, file))!
}
}
}
// check if an asset is already added to the asset manager
pub fn (am AssetManager) exists(asset_type AssetType, include_name string) bool {
assets := am.get_assets(asset_type)
return assets.any(it.include_name == include_name)
}
// include css/js files in your vweb app from templates
// Example:
// ```html
// @{app.am.include(.css, 'main.css')}
// ```
pub fn (am AssetManager) include(asset_type AssetType, include_name string) vweb.RawHtml {
assets := am.get_assets(asset_type)
for asset in assets {
if asset.include_name == include_name {
// always add link/src from root of web server ('/css/main.css'),
// but leave absolute paths intact
mut real_path := asset.file_path
if real_path[0] != `/` && !os.is_abs_path(real_path) {
real_path = '/${asset.file_path}'
}
return match asset_type {
.css {
'<link rel="stylesheet" href="${real_path}">'
}
.js {
'<script src="${real_path}"></script>'
}
else {
eprintln('[vweb.assets] can only include css or js assets')
''
}
}
}
}
eprintln('[vweb.assets] no asset with include name "${include_name}" exists!')
return ''
}
// combine assets of type `asset_type` into a single file and return the outputted file path.
// If you call `combine` with asset type `all` the function will return an empty string,
// the minified files will be available at `combined_file_name`.`asset_type`
pub fn (mut am AssetManager) combine(asset_type AssetType) !string {
if asset_type == .all {
am.combine(.css)!
am.combine(.js)!
return ''
}
if am.cache_dir == '' {
return error('cannot combine assets: cache directory is not valid')
} else if !os.exists(am.cache_dir) {
os.mkdir_all(am.cache_dir)!
}
assets := am.get_assets(asset_type)
combined_file_path := os.join_path(am.cache_dir, '${am.combined_file_name}.${asset_type}')
mut f := os.create(combined_file_path)!
for asset in assets {
bytes := os.read_bytes(asset.file_path)!
f.write(bytes)!
f.write_string('\n')!
}
f.close()
return combined_file_path
}
// TODO: implement proper minification
@[manualfree]
pub fn minify_css(css string) string {
mut lines := css.split('\n')
// estimate arbitrary number of characters for a line of css
mut sb := strings.new_builder(lines.len * 20)
defer {
unsafe { sb.free() }
}
for line in lines {
trimmed := line.trim_space()
if trimmed != '' {
sb.write_string(trimmed)
}
}
return sb.str()
}
// TODO: implement proper minification
@[manualfree]
pub fn minify_js(js string) string {
mut lines := js.split('\n')
// estimate arbitrary number of characters for a line of js
mut sb := strings.new_builder(lines.len * 40)
defer {
unsafe { sb.free() }
}
for line in lines {
trimmed := line.trim_space()
if trimmed != '' {
sb.write_string(trimmed)
sb.write_u8(` `)
}
}
return sb.str()
}

View file

@ -1,190 +0,0 @@
import x.vweb.assets
import os
const base_cache_dir = os.join_path(os.vtmp_dir(), 'xvweb_assets_test_cache')
fn testsuite_begin() {
os.mkdir_all(base_cache_dir) or {}
}
fn testsuite_end() {
os.rmdir_all(base_cache_dir) or {}
}
// clean_cache_dir used before and after tests that write to a cache directory.
// Because of parallel compilation and therefore test running,
// unique cache dirs are needed per test function.
fn clean_cache_dir(dir string) {
os.rmdir_all(dir) or {}
}
fn cache_dir(test_name string) string {
return os.join_path(base_cache_dir, test_name)
}
fn get_test_file_path(file string) string {
path := os.join_path(base_cache_dir, file)
os.rm(path) or {}
os.write_file(path, get_test_file_contents(file)) or { panic(err) }
return path
}
fn get_test_file_contents(file string) string {
contents := match file {
'test1.js' { '{"one": 1}\n' }
'test2.js' { '{"two": 2}\n' }
'test1.css' { '.one {\n\tcolor: #336699;\n}\n' }
'test2.css' { '.two {\n\tcolor: #996633;\n}\n' }
else { 'wibble\n' }
}
return contents
}
fn test_add() {
mut am := assets.AssetManager{}
mut errored := false
am.add(.css, 'test.css', 'test.css') or { errored = true }
assert errored == true, 'am.add should error'
errored = false
am.add(.css, get_test_file_path('test1.css'), 'included.css') or {
eprintln(err)
errored = true
}
assert errored == false, 'am.add should not error'
css_assets := am.get_assets(.css)
assert css_assets.len == 1
assert css_assets[0].file_path == get_test_file_path('test1.css')
assert css_assets[0].include_name == 'included.css'
}
fn test_add_minify_missing_cache_dir() {
mut am := assets.AssetManager{
minify: true
}
mut errored := false
am.add(.js, get_test_file_path('test1.css'), 'included.js') or {
assert err.msg() == 'cannot minify asset: cache directory is not valid'
errored = true
}
assert errored == true, 'am.add should return an error'
}
fn test_add_minified() {
mut am := assets.AssetManager{
minify: true
cache_dir: cache_dir('test_add_minified')
}
clean_cache_dir(am.cache_dir)
am.add(.js, get_test_file_path('test1.js'), 'included.js')!
js_assets := am.get_assets(.js)
assert js_assets.len == 1
assert js_assets[0].file_path.starts_with(am.cache_dir) == true
}
fn test_combine() {
mut am := assets.AssetManager{
cache_dir: cache_dir('test_combine')
}
clean_cache_dir(am.cache_dir)
am.add(.css, get_test_file_path('test1.css'), 'test1.css')!
am.add(.css, get_test_file_path('test2.css'), 'test2.css')!
combined_path := am.combine(.css)!
combined := os.read_file(combined_path)!
expected := get_test_file_contents('test1.css') + '\n' + get_test_file_contents('test2.css') +
'\n'
assert combined == expected
}
fn test_combine_minified() {
// minify test is simple for now, because assets are not properly minified yet
mut am := assets.AssetManager{
cache_dir: cache_dir('test_combine_minified')
minify: true
}
clean_cache_dir(am.cache_dir)
am.add(.css, get_test_file_path('test1.css'), 'test1.css')!
am.add(.css, get_test_file_path('test2.css'), 'test2.css')!
combined_path := am.combine(.css)!
combined := os.read_file(combined_path)!
// minified version should be 2 lines + one extra newline
assert combined.split('\n').len == 3
}
fn test_minify_cache_last_modified() {
mut am := assets.AssetManager{
minify: true
cache_dir: cache_dir('test_cache_last_modified')
}
clean_cache_dir(am.cache_dir)
// first we write the file and add it
am.add(.js, get_test_file_path('test1.js'), 'included.js')!
mut js_assets := am.get_assets(.js)
assert js_assets.len == 1
old_cached_path := js_assets[0].file_path
// then we only add the file, the file is not modified so the "last modified is the same".
// we expect that the asset manager doesn't cache a minified file if it hasn't been changed
// the last time it was added
am.add(.js, os.join_path(base_cache_dir, 'test1.js'), 'included.js')!
js_assets = am.get_assets(.js)
// check if the file isn't added twice
assert js_assets.len == 1
// if the file path was not modified, vweb.assets didn't overwrite the file
assert js_assets[0].file_path == old_cached_path
}
fn test_cleanup_cache() {
mut am := assets.AssetManager{
minify: true
cache_dir: cache_dir('test_cleanup_cache')
}
clean_cache_dir(am.cache_dir)
// manually make the cache dir
os.mkdir_all(am.cache_dir) or {}
// write a file to the cache dir isn't added to the asset manager to represent
// a previously cached file
path1 := os.join_path(am.cache_dir, 'test1.css')
os.write_file(path1, 'h1 { color: red; }')!
assert os.exists(path1) == true
// add a file to the asset manager and write it
am.add(.css, get_test_file_path('test2.css'), 'test2.css')!
css_assets := am.get_assets(.css)
// get the cached path
assert css_assets.len == 1
path2 := css_assets[0].file_path
assert os.exists(path2) == true
am.cleanup_cache()!
// the first asset wasn't added to the asset manager, so it should not exist
assert os.exists(path1) == false
assert os.exists(path2) == true
}
fn test_include() {
mut am := assets.AssetManager{}
css_path := get_test_file_path('test1.css')
js_path := get_test_file_path('test1.js')
am.add(.css, css_path, 'other.css')!
am.add(.js, js_path, 'js/test.js')!
assert am.include(.css, 'other.css') == '<link rel="stylesheet" href="${css_path}">'
assert am.include(.js, 'js/test.js') == '<script src="${js_path}"></script>'
}

View file

@ -1,312 +0,0 @@
module vweb
import json
import net
import net.http
import os
enum ContextReturnType {
normal
file
}
pub enum RedirectType {
found = int(http.Status.found)
moved_permanently = int(http.Status.moved_permanently)
see_other = int(http.Status.see_other)
temporary_redirect = int(http.Status.temporary_redirect)
permanent_redirect = int(http.Status.permanent_redirect)
}
// The Context struct represents the Context which holds the HTTP request and response.
// It has fields for the query, form, files and methods for handling the request and response
@[heap]
pub struct Context {
mut:
// vweb will try to infer the content type base on file extension,
// and if `content_type` is not empty the `Content-Type` header will always be
// set to this value
content_type string
// done is set to true when a response can be sent over `conn`
done bool
// if true the response should not be sent and the connection should be closed
// manually.
takeover bool
// how the http response should be handled by vweb's backend
return_type ContextReturnType = .normal
return_file string
// If the `Connection: close` header is present the connection should always be closed
client_wants_to_close bool
pub:
// TODO: move this to `handle_request`
// time.ticks() from start of vweb connection handle.
// You can use it to determine how much time is spent on your request.
page_gen_start i64
pub mut:
req http.Request
custom_mime_types map[string]string
// TCP connection to client. Only for advanced usage!
conn &net.TcpConn = unsafe { nil }
// Map containing query params for the route.
// http://localhost:3000/index?q=vpm&order_by=desc => { 'q': 'vpm', 'order_by': 'desc' }
query map[string]string
// Multipart-form fields.
form map[string]string
// Files from multipart-form.
files map[string][]http.FileData
res http.Response
// use form_error to pass errors from the context to your frontend
form_error string
livereload_poll_interval_ms int = 250
}
// returns the request header data from the key
pub fn (ctx &Context) get_header(key http.CommonHeader) !string {
return ctx.req.header.get(key)!
}
// returns the request header data from the key
pub fn (ctx &Context) get_custom_header(key string) !string {
return ctx.req.header.get_custom(key)!
}
// set a header on the response object
pub fn (mut ctx Context) set_header(key http.CommonHeader, value string) {
ctx.res.header.set(key, value)
}
// set a custom header on the response object
pub fn (mut ctx Context) set_custom_header(key string, value string) ! {
ctx.res.header.set_custom(key, value)!
}
// send_response_to_client finalizes the response headers and sets Content-Type to `mimetype`
// and the response body to `response`
pub fn (mut ctx Context) send_response_to_client(mimetype string, response string) Result {
if ctx.done && !ctx.takeover {
eprintln('[vweb] a response cannot be sent twice over one connection')
return Result{}
}
// ctx.done is only set in this function, so in order to sent a response over the connection
// this value has to be set to true. Assuming the user doesn't use `ctx.conn` directly.
ctx.done = true
ctx.res.body = response
$if vweb_livereload ? {
if mimetype == 'text/html' {
ctx.res.body = response.replace('</html>', '<script src="/vweb_livereload/${vweb_livereload_server_start}/script.js"></script>\n</html>')
}
}
// set Content-Type and Content-Length headers
mut custom_mimetype := if ctx.content_type.len == 0 { mimetype } else { ctx.content_type }
ctx.res.header.set(.content_type, custom_mimetype)
if ctx.res.body != '' {
ctx.res.header.set(.content_length, ctx.res.body.len.str())
}
// send vweb's closing headers
ctx.res.header.set(.server, 'VWeb')
if !ctx.takeover && ctx.client_wants_to_close {
// Only sent the `Connection: close` header when the client wants to close
// the connection. This typically happens when the client only supports HTTP 1.0
ctx.res.header.set(.connection, 'close')
}
// set the http version
ctx.res.set_version(.v1_1)
if ctx.res.status_code == 0 {
ctx.res.set_status(.ok)
}
if ctx.takeover {
fast_send_resp(mut ctx.conn, ctx.res) or {}
}
// result is send in `vweb.v`, `handle_route`
return Result{}
}
// Response with payload and content-type `text/html`
pub fn (mut ctx Context) html(s string) Result {
return ctx.send_response_to_client('text/html', s)
}
// Response with `s` as payload and content-type `text/plain`
pub fn (mut ctx Context) text(s string) Result {
return ctx.send_response_to_client('text/plain', s)
}
// Response with json_s as payload and content-type `application/json`
pub fn (mut ctx Context) json[T](j T) Result {
json_s := json.encode(j)
return ctx.send_response_to_client('application/json', json_s)
}
// Response with a pretty-printed JSON result
pub fn (mut ctx Context) json_pretty[T](j T) Result {
json_s := json.encode_pretty(j)
return ctx.send_response_to_client('application/json', json_s)
}
// Response HTTP_OK with file as payload
pub fn (mut ctx Context) file(file_path string) Result {
if !os.exists(file_path) {
eprintln('[vweb] file "${file_path}" does not exist')
return ctx.not_found()
}
ext := os.file_ext(file_path)
mut content_type := ctx.content_type
if content_type.len == 0 {
if ct := ctx.custom_mime_types[ext] {
content_type = ct
} else {
content_type = mime_types[ext]
}
}
if content_type.len == 0 {
eprintln('[vweb] no MIME type found for extension "${ext}"')
return ctx.server_error('')
}
return ctx.send_file(content_type, file_path)
}
fn (mut ctx Context) send_file(content_type string, file_path string) Result {
mut file := os.open(file_path) or {
eprint('[vweb] error while trying to open file: ${err.msg()}')
ctx.res.set_status(.not_found)
return ctx.text('resource does not exist')
}
// seek from file end to get the file size
file.seek(0, .end) or {
eprintln('[vweb] error while trying to read file: ${err.msg()}')
return ctx.server_error('could not read resource')
}
file_size := file.tell() or {
eprintln('[vweb] error while trying to read file: ${err.msg()}')
return ctx.server_error('could not read resource')
}
file.close()
if ctx.takeover {
// it's a small file so we can send the response directly
data := os.read_file(file_path) or {
eprintln('[vweb] error while trying to read file: ${err.msg()}')
return ctx.server_error('could not read resource')
}
return ctx.send_response_to_client(content_type, data)
} else {
ctx.return_type = .file
ctx.return_file = file_path
// set response headers
ctx.send_response_to_client(content_type, '')
ctx.res.header.set(.content_length, file_size.str())
return Result{}
}
}
// Response HTTP_OK with s as payload
pub fn (mut ctx Context) ok(s string) Result {
mut mime := if ctx.content_type.len == 0 { 'text/plain' } else { ctx.content_type }
return ctx.send_response_to_client(mime, s)
}
// send an error 400 with a message
pub fn (mut ctx Context) request_error(msg string) Result {
ctx.res.set_status(.bad_request)
return ctx.send_response_to_client('text/plain', msg)
}
// send an error 500 with a message
pub fn (mut ctx Context) server_error(msg string) Result {
ctx.res.set_status(.internal_server_error)
return ctx.send_response_to_client('text/plain', msg)
}
@[params]
pub struct RedirectParams {
pub:
typ RedirectType
}
// Redirect to an url
pub fn (mut ctx Context) redirect(url string, params RedirectParams) Result {
status := http.Status(params.typ)
ctx.res.set_status(status)
ctx.res.header.add(.location, url)
return ctx.send_response_to_client('text/plain', status.str())
}
// before_request is always the first function that is executed and acts as middleware
pub fn (mut ctx Context) before_request() Result {
return Result{}
}
// returns a HTTP 404 response
pub fn (mut ctx Context) not_found() Result {
ctx.res.set_status(.not_found)
return ctx.send_response_to_client('text/plain', '404 Not Found')
}
// Gets a cookie by a key
pub fn (ctx &Context) get_cookie(key string) ?string {
if cookie := ctx.req.cookie(key) {
return cookie.value
} else {
return none
}
}
// Sets a cookie
pub fn (mut ctx Context) set_cookie(cookie http.Cookie) {
cookie_raw := cookie.str()
if cookie_raw == '' {
eprintln('[vweb] error setting cookie: name of cookie is invalid.\n${cookie}')
return
}
ctx.res.header.add(.set_cookie, cookie_raw)
}
// set_content_type sets the Content-Type header to `mime`
pub fn (mut ctx Context) set_content_type(mime string) {
ctx.content_type = mime
}
// takeover_conn prevents vweb from automatically sending a response and closing
// the connection. You are responsible for closing the connection.
// In takeover mode if you call a Context method the response will be directly
// send over the connection and you can send multiple responses.
// This function is useful when you want to keep the connection alive and/or
// send multiple responses. Like with the SSE.
pub fn (mut ctx Context) takeover_conn() {
ctx.takeover = true
}
// user_agent returns the user-agent header for the current client
pub fn (ctx &Context) user_agent() string {
return ctx.req.header.get(.user_agent) or { '' }
}
// Returns the ip address from the current user
pub fn (ctx &Context) ip() string {
mut ip := ctx.req.header.get_custom('CF-Connecting-IP') or { '' }
if ip == '' {
ip = ctx.req.header.get(.x_forwarded_for) or { '' }
}
if ip == '' {
ip = ctx.req.header.get_custom('X-Forwarded-For') or { '' }
}
if ip == '' {
ip = ctx.req.header.get_custom('X-Real-Ip') or { '' }
}
if ip.contains(',') {
ip = ip.all_before(',')
}
if ip == '' {
ip = ctx.conn.peer_ip() or { '' }
}
return ip
}

View file

@ -1,112 +0,0 @@
module vweb
import net.urllib
type ControllerHandler = fn (ctx &Context, mut url urllib.URL, host string) &Context
pub struct ControllerPath {
pub:
path string
handler ControllerHandler = unsafe { nil }
pub mut:
host string
}
interface ControllerInterface {
controllers []&ControllerPath
}
pub struct Controller {
pub mut:
controllers []&ControllerPath
}
// register_controller adds a new Controller to your app
pub fn (mut c Controller) register_controller[A, X](path string, mut global_app A) ! {
c.controllers << controller[A, X](path, mut global_app)!
}
// controller generates a new Controller for the main app
pub fn controller[A, X](path string, mut global_app A) !&ControllerPath {
routes := generate_routes[A, X](global_app) or { panic(err.msg()) }
controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)!
// generate struct with closure so the generic type is encapsulated in the closure
// no need to type `ControllerHandler` as generic since it's not needed for closures
return &ControllerPath{
path: path
handler: fn [mut global_app, path, routes, controllers_sorted] [A, X](ctx &Context, mut url urllib.URL, host string) &Context {
// transform the url
url.path = url.path.all_after_first(path)
// match controller paths
$if A is ControllerInterface {
if completed_context := handle_controllers[X](controllers_sorted, ctx, mut
url, host)
{
return completed_context
}
}
// create a new user context and pass the vweb's context
mut user_context := X{}
user_context.Context = ctx
handle_route[A, X](mut global_app, mut user_context, url, host, &routes)
// we need to explicitly tell the V compiler to return a reference
return &user_context.Context
}
}
}
// register_controller adds a new Controller to your app
pub fn (mut c Controller) register_host_controller[A, X](host string, path string, mut global_app A) ! {
c.controllers << controller_host[A, X](host, path, mut global_app)!
}
// controller_host generates a controller which only handles incoming requests from the `host` domain
pub fn controller_host[A, X](host string, path string, mut global_app A) &ControllerPath {
mut ctrl := controller[A, X](path, mut global_app)
ctrl.host = host
return ctrl
}
fn check_duplicate_routes_in_controllers[T](global_app &T, routes map[string]Route) ![]&ControllerPath {
mut controllers_sorted := []&ControllerPath{}
$if T is ControllerInterface {
mut paths := []string{}
controllers_sorted = global_app.controllers.clone()
controllers_sorted.sort(a.path.len > b.path.len)
for controller in controllers_sorted {
if controller.host == '' {
if controller.path in paths {
return error('conflicting paths: duplicate controller handling the route "${controller.path}"')
}
paths << controller.path
}
}
for method_name, route in routes {
for controller_path in paths {
if route.path.starts_with(controller_path) {
return error('conflicting paths: method "${method_name}" with route "${route.path}" should be handled by the Controller of path "${controller_path}"')
}
}
}
}
return controllers_sorted
}
fn handle_controllers[X](controllers []&ControllerPath, ctx &Context, mut url urllib.URL, host string) ?&Context {
for controller in 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
return controller.handler(ctx, mut url, host)
}
}
return none
}

View file

@ -1,230 +0,0 @@
# Cross-Site Request Forgery (CSRF) protection
This module implements the [double submit cookie][owasp] technique to protect routes
from CSRF attacks.
CSRF is a type of attack that occurs when a malicious program/website (and others) causes
a user's web browser to perform an action without them knowing. A web browser automatically sends
cookies to a website when it performs a request, including session cookies. So if a user is
authenticated on your website the website can not distinguish a forged request by a legitimate
request.
## When to not add CSRF-protection
If you are creating a service that is intended to be used by other servers e.g. an API,
you probably don't want CSRF-protection. An alternative would be to send an Authorization
token in, and only in, an HTTP-header (like JSON Web Tokens). If you do that your website
isn't vulnerable to CSRF-attacks.
## Usage
To enable CSRF-protection for your vweb app you must embed the `CsrfContext` struct
on your `Context` struct. You must also provide configuration options
(see [configuration & security](#configuration--security-considerations)).
**Example:**
```v
import x.vweb
import x.vweb.csrf
pub struct Context {
vweb.Context
csrf.CsrfContext
}
```
Change `secret` and `allowed_hosts` in a production environment!
**Example:**
```v ignore
const csrf_config := csrf.CsrfConfig{
secret: 'my-secret'
allowed_hosts: ['*']
}
```
### Middleware
Enable CSRF protection for all routes, or a certain route(s) by using vweb's middleware.
**Example:**
```v ignore
pub struct App {
vweb.Middleware[Context]
}
fn main() {
mut app := &App{}
// register the CSRF middleware and pass our configuration
// protect a specific route
app.route_use('/login', csrf.middleware[Context](csrf_config))
vweb.run[App, Context](mut app, 8080)
}
```
### Setting the token
For the CSRF-protection to work we have to generate an anti-CSRF token and set it
as an hidden input field on any form that will be submitted to the route we
want to protect.
**Example:**
*main.v*
```v ignore
fn (app &App) index(mut ctx) vweb.Result {
// this function will set a cookie header and generate a CSRF token
ctx.set_csrf_token(mut ctx)
return $vweb.html()
}
@[post]
fn (app &App) login(mut ctx, password string) vweb.Result {
// implement your own password validation here
if password == 'password' {
return ctx.text('You are logged in!')
} else {
return ctx.text('Invalid password!')
}
}
```
*templates/index.html*
```html
<h1>Log in</h1>
<form method="POST" action="/login">
@{ctx.csrf_token_input()}
<label for="password">Password:</label>
<input type="text" name="password" id="password">
<button type="submit">Log in</button>
</form>
```
If we run the app with `v run main.v` and navigate to `http://localhost:8080/`
we will see the login form and we can login using the password "password".
If we remove the hidden input, by removing the line `@{ctx.csrf_token_input()}`
from our html code we will see an error message indicating that the CSRF token
is not set or invalid! By default the CSRF module sends an HTTP-403 response when
a token is invalid, if you want to send a custom response see the
[advanced usage](#advanced-usage) section.
> **Note:**
> Please read the security and configuration section! If you configure
> the CSRF module in an unsafe way, the protection will be useless.
## Advanced Usage
If you want more control over what routes are protected or what action you want to
do when a CSRF-token is invalid, you can call `csrf.protect` yourself whenever you want
to protect a route against CSRF attacks. This function returns `false` if the current CSRF token
and cookie combination is not valid.
**Example:**
```v ignore
@[post]
fn (app &App) login(mut ctx, password string) vweb.Result {
if csrf.protect(mut ctx, csrf_config) == false {
// CSRF verification failed!
}
// ...
}
```
### Obtaining the anti-CSRF token
When `set_csrf_token` is called the token is stored in the `csrf_token` field. You access
this field directly to use it in an input field, or call `csrf_token_input`.
**Example:**
```v ignore
fn (app &App) index(mut ctx) vweb.Result {
token := ctx.set_csrf_token(mut ctx)
}
```
### Clearing the anti-CSRF token
If you want to remove the anti-CSRF token and the cookie header you can call `clear_csrf_token`
**Example:**
```v ignore
ctx.clear_csrf_token()
```
## How it works
This module implements the [double submit cookie][owasp] technique: a random token
is generated, the CSRF-token. The hmac of this token and the secret key is stored in a cookie.
When a request is made, the CSRF-token should be placed inside a HTML form element.
The CSRF-token the hmac of the CSRF-token in the formdata is compared to the cookie.
If the values match, the request is accepted.
This approach has the advantage of being stateless: there is no need to store tokens on the server
side and validate them. The token and cookie are bound cryptographically to each other so
an attacker would need to know both values in order to make a CSRF-attack succeed. That
is why is it important to **not leak the CSRF-token** via an url, or some other way. This is way
by default the `HTTPOnly` flag on the cookie is set to true.
See [client side CSRF][client-side-csrf] for more information.
This is a high level overview of the implementation.
## Configuration & Security Considerations
### The secret key
The secret key should be a random string that is not easily guessable.
### Sessions
If your app supports some kind of user sessions, it is recommended to cryptographically
bind the CSRF-token to the users' session. You can do that by providing the name
of the session ID cookie. If an attacker changes the session ID in the cookie, in the
token or both the hmac will be different and the request will be rejected.
**Example**:
```v ignore
csrf_config = csrf.CsrfConfig{
// ...
session_cookie: 'my_session_id_cookie_name'
}
```
### Safe Methods
The HTTP methods `GET`, `OPTIONS`, `HEAD` are considered
[safe methods][mozilla-safe-methods] meaning they should not alter the state of
an application. If a request with a "safe method" is made, the csrf protection will be skipped.
You can change which methods are considered safe by changing `CsrfConfig.safe_methods`.
### Allowed Hosts
By default, both the http Origin and Referer headers are checked and matched strictly
to the values in `allowed_hosts`. That means that you need to include each subdomain.
If the value of `allowed_hosts` contains the wildcard: `'*'` the headers will not be checked.
#### Domain name matching
The following configuration will not allow requests made from `test.example.com`,
only from `example.com`.
**Example**
```v ignore
config := csrf.CsrfConfig{
secret: '...'
allowed_hosts: ['example.com']
}
```
#### Referer, Origin header check
In some cases (like if your server is behind a proxy), the Origin or Referer header will
not be present. If that is your case you can set `check_origin_and_referer` to `false`.
Request will now be accepted when the Origin *or* Referer header is valid.
### Share csrf cookie with subdomains
If you need to share the CSRF-token cookie with subdomains, you can set
`same_site` to `.same_site_lax_mode`.
## Configuration
All configuration options are defined in `CsrfConfig`.
[//]: # (Sources)
[owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
[client-side-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#client-side-csrf
[mozilla-safe-methods]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP

View file

@ -1,229 +0,0 @@
module csrf
import crypto.hmac
import crypto.sha256
import encoding.base64
import net.http
import net.urllib
import rand
import time
import x.vweb
@[params]
pub struct CsrfConfig {
pub:
secret string
// how long the random part of the csrf-token should be
nonce_length int = 64
// HTTP "safe" methods meaning they shouldn't alter state.
// If a request with any of these methods is made, `protect` will always return true
// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
safe_methods []http.Method = [.get, .head, .options]
// which hosts are allowed, enforced by checking the Origin and Referer header
// if allowed_hosts contains '*' the check will be skipped.
// Subdomains need to be included separately: a request from `"sub.example.com"`
// will be rejected when `allowed_host = ['example.com']`.
allowed_hosts []string
// if set to true both the Referer and Origin headers must match `allowed_hosts`
// else if either one is valid the request is accepted
check_origin_and_referer bool = true
// the name of the csrf-token in the hidden html input
token_name string = 'csrftoken'
// the name of the cookie that contains the session id
session_cookie string
// cookie options
cookie_name string = 'csrftoken'
same_site http.SameSite = .same_site_strict_mode
cookie_path string = '/'
// how long the cookie stays valid in seconds. Default is 30 days
max_age int = 60 * 60 * 24 * 30
cookie_domain string
// whether the cookie can be send only over HTTPS
secure bool
}
pub struct CsrfContext {
pub mut:
config CsrfConfig
exempt bool
// the csrftoken that should be placed in an html form
csrf_token string
}
// set_token generates a new csrf_token and adds a Cookie to the response
pub fn (mut ctx CsrfContext) set_csrf_token[T](mut user_context T) string {
ctx.csrf_token = set_token(mut user_context, ctx.config)
return ctx.csrf_token
}
// clear the csrf token and cookie header from the context
pub fn (ctx &CsrfContext) clear_csrf_token[T](mut user_context T) {
user_context.set_cookie(http.Cookie{
name: config.cookie_name
value: ''
max_age: 0
})
}
// csrf_token_input returns an HTML hidden input containing the csrf token
pub fn (ctx &CsrfContext) csrf_token_input() vweb.RawHtml {
return '<input type="hidden" name="${ctx.config.token_name}" value="${ctx.csrf_token}">'
}
// middleware returns a handler that you can use with vweb's middleware
pub fn middleware[T](config CsrfConfig) vweb.MiddlewareOptions[T] {
return vweb.MiddlewareOptions[T]{
after: false
handler: fn [config] [T](mut ctx T) bool {
ctx.config = config
if ctx.exempt {
return true
} else if ctx.req.method in config.safe_methods {
return true
} else {
return protect(mut ctx, config)
}
}
}
}
// set_token returns the csrftoken and sets an encrypted cookie with the hmac of
// `config.get_secret` and the csrftoken
pub fn set_token(mut ctx vweb.Context, config &CsrfConfig) string {
expire_time := time.now().add_seconds(config.max_age)
session_id := ctx.get_cookie(config.session_cookie) or { '' }
token := generate_token(expire_time.unix(), session_id, config.nonce_length)
cookie := generate_cookie(expire_time.unix(), token, config.secret)
// the hmac key is set as a cookie and later validated with `app.token` that must
// be in an html form
ctx.set_cookie(http.Cookie{
name: config.cookie_name
value: cookie
same_site: config.same_site
http_only: true
secure: config.secure
path: config.cookie_path
expires: expire_time
max_age: config.max_age
})
return token
}
// protect returns false and sends an http 401 response when the csrf verification
// fails. protect will always return true if the current request method is in
// `config.safe_methods`.
pub fn protect(mut ctx vweb.Context, config &CsrfConfig) bool {
// if the request method is a "safe" method we allow the request
if ctx.req.method in config.safe_methods {
return true
}
// check origin and referer header
if check_origin_and_referer(ctx, config) == false {
request_is_invalid(mut ctx)
return false
}
// use the session id from the cookie, not from the csrftoken
session_id := ctx.get_cookie(config.session_cookie) or { '' }
actual_token := ctx.form[config.token_name] or {
request_is_invalid(mut ctx)
return false
}
// retrieve timestamp and nonce from csrftoken
data := base64.url_decode_str(actual_token).split('.')
println(data)
if data.len < 3 {
request_is_invalid(mut ctx)
return false
}
// check the timestamp from the csrftoken against the current time
// if an attacker would change the timestamp on the cookie, the token or both the
// hmac would also change.
now := time.now().unix()
expire_timestamp := data[0].i64()
if expire_timestamp < now {
// token has expired
request_is_invalid(mut ctx)
return false
}
nonce := data.last()
expected_token := base64.url_encode_str('${expire_timestamp}.${session_id}.${nonce}')
mut actual_hash := ctx.get_cookie(config.cookie_name) or {
request_is_invalid(mut ctx)
return false
}
// old_expire := actual_hash.all_before('.')
// actual_hash = actual_hash.replace_once (old_expire, expire_timestamp.str())
// generate new hmac based on information in the http request
expected_hash := generate_cookie(expire_timestamp, expected_token, config.secret)
eprintln(actual_hash)
eprintln(expected_hash)
// if the new hmac matches the cookie value the request is legit
if actual_hash != expected_hash {
request_is_invalid(mut ctx)
return false
}
eprintln('matching')
return true
}
// check_origin_and_referer validates the `Origin` and `Referer` headers.
fn check_origin_and_referer(ctx vweb.Context, config &CsrfConfig) bool {
// wildcard allow all hosts NOT SAFE!
if '*' in config.allowed_hosts {
return true
}
// only match host and match the full domain name
// because lets say `allowed_host` = `['example.com']`.
// Attackers shouldn't be able to bypass this check with the domain `example.com.attacker.com`
origin := ctx.get_header(.origin) or { return false }
origin_url := urllib.parse(origin) or { urllib.URL{} }
valid_origin := origin_url.hostname() in config.allowed_hosts
referer := ctx.get_header(.referer) or { return false }
referer_url := urllib.parse(referer) or { urllib.URL{} }
valid_referer := referer_url.hostname() in config.allowed_hosts
if config.check_origin_and_referer {
return valid_origin && valid_referer
} else {
return valid_origin || valid_referer
}
}
// request_is_invalid sends an http 403 response
fn request_is_invalid(mut ctx vweb.Context) {
ctx.res.set_status(.forbidden)
ctx.text('Forbidden: Invalid or missing CSRF token')
}
fn generate_token(expire_time i64, session_id string, nonce_length int) string {
nonce := rand.string_from_set('0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz',
nonce_length)
token := '${expire_time}.${session_id}.${nonce}'
return base64.url_encode_str(token)
}
// generate_cookie converts secret key based on the request context and a random
// token into an hmac key
fn generate_cookie(expire_time i64, token string, secret string) string {
hash := base64.url_encode(hmac.new(secret.bytes(), token.bytes(), sha256.sum, sha256.block_size))
cookie := '${expire_time}.${hash}'
return cookie
}

View file

@ -1,327 +0,0 @@
import time
import net.http
import net.html
import os
import x.vweb
import x.vweb.csrf
const sport = 12385
const localserver = '127.0.0.1:${sport}'
const exit_after_time = 12000 // milliseconds
const session_id_cookie_name = 'session_id'
const csrf_config = &csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: ['*']
session_cookie: session_id_cookie_name
}
const allowed_origin = 'example.com'
const csrf_config_origin = csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: [allowed_origin]
session_cookie: session_id_cookie_name
}
// Test CSRF functions
// =====================================
fn test_set_token() {
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
cookie := ctx.res.header.get(.set_cookie) or { '' }
assert cookie.len != 0
assert cookie.starts_with('${csrf_config.cookie_name}=')
}
fn test_protect() {
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
mut cookie := ctx.res.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
form := {
csrf_config.token_name: token
}
ctx = vweb.Context{
form: form
req: http.Request{
method: .post
}
}
ctx.req.add_cookie(name: csrf_config.cookie_name, value: cookie)
valid := csrf.protect(mut ctx, csrf_config)
assert valid == true
}
fn test_timeout() {
timeout := 1
short_time_config := &csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: ['*']
session_cookie: session_id_cookie_name
max_age: timeout
}
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, short_time_config)
// after 2 seconds the cookie should expire (maxage)
time.sleep(2 * time.second)
mut cookie := ctx.res.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
form := {
short_time_config.token_name: token
}
ctx = vweb.Context{
form: form
req: http.Request{
method: .post
}
}
ctx.req.add_cookie(name: short_time_config.cookie_name, value: cookie)
valid := csrf.protect(mut ctx, short_time_config)
assert valid == false
}
fn test_valid_origin() {
// valid because both Origin and Referer headers are present
token, cookie := get_token_cookie('')
form := {
csrf_config.token_name: token
}
mut req := http.Request{
method: .post
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
req.add_header(.origin, 'http://${allowed_origin}')
req.add_header(.referer, 'http://${allowed_origin}/test')
mut ctx := vweb.Context{
form: form
req: req
}
mut valid := csrf.protect(mut ctx, csrf_config_origin)
assert valid == true
}
fn test_invalid_origin() {
// invalid because either the Origin, Referer or neither are present
token, cookie := get_token_cookie('')
form := {
csrf_config.token_name: token
}
mut req := http.Request{
method: .post
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
req.add_header(.origin, 'http://${allowed_origin}')
mut ctx := vweb.Context{
form: form
req: req
}
mut valid := csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
req = http.Request{
method: .post
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
req.add_header(.referer, 'http://${allowed_origin}/test')
ctx = vweb.Context{
form: form
req: req
}
valid = csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
req = http.Request{
method: .post
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
ctx = vweb.Context{
form: form
req: req
}
valid = csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
}
// Testing App
// ================================
pub struct Context {
vweb.Context
csrf.CsrfContext
}
pub struct App {
vweb.Middleware[Context]
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
fn (app &App) index(mut ctx Context) vweb.Result {
ctx.set_csrf_token(mut ctx)
return ctx.html('<form action="/auth" method="post">
${ctx.csrf_token_input()}
<label for="password">Your password:</label>
<input type="text" id="password" name="password" placeholder="Your password" />
</form>')
}
@[post]
fn (app &App) auth(mut ctx Context) vweb.Result {
return ctx.ok('authenticated')
}
// App cleanup function
// ======================================
pub fn (mut app App) shutdown(mut ctx Context) vweb.Result {
spawn app.exit_gracefully()
return ctx.ok('good bye')
}
fn (app &App) exit_gracefully() {
eprintln('>> webserver: exit_gracefully')
time.sleep(100 * time.millisecond)
exit(0)
}
fn exit_after_timeout[T](mut app T, timeout_in_ms int) {
time.sleep(timeout_in_ms * time.millisecond)
eprintln('>> webserver: pid: ${os.getpid()}, exiting ...')
app.exit_gracefully()
eprintln('App timed out!')
assert true == false
}
// Tests for the App
// ======================================
fn test_run_app_in_background() {
mut app := &App{}
app.route_use('/auth', csrf.middleware[Context](csrf_config))
spawn exit_after_timeout(mut app, exit_after_time)
spawn vweb.run_at[App, Context](mut app, port: sport, family: .ip)
_ := <-app.started
}
fn test_token_input() {
res := http.get('http://${localserver}/') or { panic(err) }
mut doc := html.parse(res.body)
inputs := doc.get_tags_by_attribute_value('type', 'hidden')
assert inputs.len == 1
assert csrf_config.token_name == inputs[0].attributes['name']
}
// utility function to check whether the route at `path` is protected against csrf
fn protect_route_util(path string) {
mut req := http.Request{
method: .post
url: 'http://${localserver}/${path}'
}
mut res := req.do() or { panic(err) }
assert res.status() == .forbidden
// A valid request with CSRF protection should have a cookie session id,
// csrftoken in `app.form` and the hmac of that token in a cookie
session_id := 'user_session_id'
token, cookie := get_token_cookie(session_id)
header := http.new_header_from_map({
http.CommonHeader.origin: 'http://${allowed_origin}'
http.CommonHeader.referer: 'http://${allowed_origin}/route'
})
formdata := http.url_encode_form_data({
csrf_config.token_name: token
})
// session id is altered: test if session hijacking is possible
// if the session id the csrftoken changes so the cookie can't be validated
mut cookies := {
csrf_config.cookie_name: cookie
session_id_cookie_name: 'altered'
}
req = http.Request{
method: .post
url: 'http://${localserver}/${path}'
data: formdata
header: header
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
req.add_cookie(name: session_id_cookie_name, value: 'altered')
res = req.do() or { panic(err) }
assert res.status() == .forbidden
req = http.Request{
method: .post
url: 'http://${localserver}/${path}'
data: formdata
header: header
}
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
req.add_cookie(name: session_id_cookie_name, value: session_id)
// Everything is valid now and the request should succeed, since session_id_cookie_name will be session_id
res = req.do() or { panic(err) }
assert res.status() == .ok
}
fn test_protect_app() {
protect_route_util('/auth')
}
fn testsuite_end() {
// This test is guaranteed to be called last.
// It sends a request to the server to shutdown.
x := http.get('http://${localserver}/shutdown') or {
assert err.msg() == ''
return
}
assert x.status() == .ok
assert x.body == 'good bye'
}
// Utility functions
fn get_token_cookie(session_id string) (string, string) {
mut ctx := vweb.Context{}
ctx.req.add_cookie(name: session_id_cookie_name, value: session_id)
token := csrf.set_token(mut ctx, csrf_config_origin)
mut cookie := ctx.res.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
return token, cookie
}

View file

@ -1,11 +0,0 @@
module vweb
import encoding.html
// Do not delete.
// Calls to this function are generated by `fn (mut g Gen) str_val(node ast.StringInterLiteral, i int, fmts []u8) {` in vlib/v/gen/c/str_intp.v,
// for string interpolation inside vweb templates.
// TODO: move it to template render
fn filter(s string) string {
return html.escape(s)
}

View file

@ -1,323 +0,0 @@
module vweb
import compress.gzip
import net.http
pub type MiddlewareHandler[T] = fn (mut T) bool
// TODO: get rid of this `voidptr` interface check when generic embedded
// interfaces work properly, related: #19968
interface MiddlewareApp {
mut:
global_handlers []voidptr
global_handlers_after []voidptr
route_handlers []RouteMiddleware
route_handlers_after []RouteMiddleware
}
struct RouteMiddleware {
url_parts []string
handler voidptr
}
pub struct Middleware[T] {
mut:
global_handlers []voidptr
global_handlers_after []voidptr
route_handlers []RouteMiddleware
route_handlers_after []RouteMiddleware
}
@[params]
pub struct MiddlewareOptions[T] {
pub:
handler fn (mut ctx T) bool @[required]
after bool
}
// string representation of Middleware
pub fn (m &Middleware[T]) str() string {
return 'vweb.Middleware[${T.name}]{
global_handlers: [${m.global_handlers.len}]
global_handlers_after: [${m.global_handlers_after.len}]
route_handlers: [${m.route_handlers.len}]
route_handlers_after: [${m.route_handlers_after.len}]
}'
}
// use registers a global middleware handler
pub fn (mut m Middleware[T]) use(options MiddlewareOptions[T]) {
if options.after {
m.global_handlers_after << voidptr(options.handler)
} else {
m.global_handlers << voidptr(options.handler)
}
}
// route_use registers a middleware handler for a specific route(s)
pub fn (mut m Middleware[T]) route_use(route string, options MiddlewareOptions[T]) {
middleware := RouteMiddleware{
url_parts: route.split('/').filter(it != '')
handler: voidptr(options.handler)
}
if options.after {
m.route_handlers_after << middleware
} else {
m.route_handlers << middleware
}
}
fn (m &Middleware[T]) get_handlers_for_route(route_path string) []voidptr {
mut fns := []voidptr{}
route_parts := route_path.split('/').filter(it != '')
for handler in m.route_handlers {
if _ := route_matches(route_parts, handler.url_parts) {
fns << handler.handler
} else if handler.url_parts.len == 0 && route_path == '/index' {
fns << handler.handler
}
}
return fns
}
fn (m &Middleware[T]) get_handlers_for_route_after(route_path string) []voidptr {
mut fns := []voidptr{}
route_parts := route_path.split('/').filter(it != '')
for handler in m.route_handlers_after {
if _ := route_matches(route_parts, handler.url_parts) {
fns << handler.handler
} else if handler.url_parts.len == 0 && route_path == '/index' {
fns << handler.handler
}
}
return fns
}
fn (m &Middleware[T]) get_global_handlers() []voidptr {
return m.global_handlers
}
fn (m &Middleware[T]) get_global_handlers_after() []voidptr {
return m.global_handlers_after
}
fn validate_middleware[T](mut ctx T, raw_handlers []voidptr) bool {
for handler in raw_handlers {
func := MiddlewareHandler[T](handler)
if func(mut ctx) == false {
return false
}
}
return true
}
// encode_gzip adds gzip encoding to the HTTP Response body.
// This middleware does not encode files, if you return `ctx.file()`.
// Register this middleware as last!
// Example: app.use(vweb.encode_gzip[Context]())
pub fn encode_gzip[T]() MiddlewareOptions[T] {
return MiddlewareOptions[T]{
after: true
handler: fn [T](mut ctx T) bool {
// TODO: compress file in streaming manner, or precompress them?
if ctx.return_type == .file {
return true
}
// first try compressions, because if it fails we can still send a response
// before taking over the connection
compressed := gzip.compress(ctx.res.body.bytes()) or {
eprintln('[vweb] error while compressing with gzip: ${err.msg()}')
return true
}
// enables us to have full control over what response is send over the connection
// and how.
ctx.takeover_conn()
// set HTTP headers for gzip
ctx.res.header.add(.content_encoding, 'gzip')
ctx.res.header.set(.vary, 'Accept-Encoding')
ctx.res.header.set(.content_length, compressed.len.str())
fast_send_resp_header(mut ctx.Context.conn, ctx.res) or {}
ctx.Context.conn.write_ptr(&u8(compressed.data), compressed.len) or {}
ctx.Context.conn.close() or {}
return false
}
}
}
// decode_gzip decodes the body of a gzip'ed HTTP request.
// Register this middleware before you do anything with the request body!
// Example: app.use(vweb.decode_gzip[Context]())
pub fn decode_gzip[T]() MiddlewareOptions[T] {
return MiddlewareOptions[T]{
handler: fn [T](mut ctx T) bool {
if encoding := ctx.res.header.get(.content_encoding) {
if encoding == 'gzip' {
decompressed := gzip.decompress(ctx.req.body.bytes()) or {
ctx.request_error('invalid gzip encoding')
return false
}
ctx.req.body = decompressed.bytestr()
}
}
}
}
}
interface HasBeforeRequest {
before_request()
}
pub const cors_safelisted_response_headers = [http.CommonHeader.cache_control, .content_language,
.content_length, .content_type, .expires, .last_modified, .pragma].map(it.str())
// CorsOptions is used to set CORS response headers.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers
@[params]
pub struct CorsOptions {
pub:
// from which origin(s) can cross-origin requests be made; `Access-Control-Allow-Origin`
origins []string @[required]
// indicate whether the server allows credentials, e.g. cookies, in cross-origin requests.
// ;`Access-Control-Allow-Credentials`
allow_credentials bool
// allowed HTTP headers for a cross-origin request; `Access-Control-Allow-Headers`
allowed_headers []string = ['*']
// allowed HTTP methods for a cross-origin request; `Access-Control-Allow-Methods`
allowed_methods []http.Method
// indicate if clients are able to access other headers than the "CORS-safelisted"
// response headers; `Access-Control-Expose-Headers`
expose_headers []string
// how long the results of a preflight request can be cached, value is in seconds
// ; `Access-Control-Max-Age`
max_age ?int
}
// set_headers adds the CORS headers on the response
pub fn (options &CorsOptions) set_headers(mut ctx Context) {
// A browser will reject a CORS request when the Access-Control-Allow-Origin header
// is not present. By not setting the CORS headers when an invalid origin is supplied
// we force the browser to reject the preflight and the actual request.
origin := ctx.req.header.get(.origin) or { return }
if options.origins != ['*'] && origin !in options.origins {
return
}
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.vary, 'Origin, Access-Control-Request-Headers')
// dont' set the value of `Access-Control-Allow-Credentials` to 'false', but
// omit the header if the value is `false`
if options.allow_credentials {
ctx.set_header(.access_control_allow_credentials, 'true')
}
if options.allowed_headers.len > 0 {
ctx.set_header(.access_control_allow_headers, options.allowed_headers.join(','))
} else if _ := ctx.req.header.get(.access_control_request_headers) {
// a server must respond with `Access-Control-Allow-Headers` if
// `Access-Control-Request-Headers` is present in a preflight request
ctx.set_header(.access_control_allow_headers, cors_safelisted_response_headers.join(','))
}
if options.allowed_methods.len > 0 {
method_str := options.allowed_methods.str().trim('[]')
ctx.set_header(.access_control_allow_methods, method_str)
}
if options.expose_headers.len > 0 {
ctx.set_header(.access_control_expose_headers, options.expose_headers.join(','))
}
if max_age := options.max_age {
ctx.set_header(.access_control_max_age, max_age.str())
}
}
// validate_request checks if a cross-origin request is made and verifies the CORS
// headers. If a cross-origin request is invalid this method will send a response
// using `ctx`.
pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
origin := ctx.req.header.get(.origin) or { return true }
if options.origins != ['*'] && origin !in options.origins {
ctx.res.set_status(.forbidden)
ctx.text('invalid CORS origin')
$if vweb_trace_cors ? {
eprintln('[vweb]: rejected CORS request from "${origin}". Reason: invalid origin')
}
return false
}
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.vary, 'Origin, Access-Control-Request-Headers')
// validate request method
if ctx.req.method !in options.allowed_methods {
ctx.res.set_status(.method_not_allowed)
ctx.text('${ctx.req.method} requests are not allowed')
$if vweb_trace_cors ? {
eprintln('[vweb]: rejected CORS request from "${origin}". Reason: invalid request method: ${ctx.req.method}')
}
return false
}
if options.allowed_headers.len > 0 && options.allowed_headers != ['*'] {
// validate request headers
for header in ctx.req.header.keys() {
if header !in options.allowed_headers {
ctx.res.set_status(.forbidden)
ctx.text('invalid Header "${header}"')
$if vweb_trace_cors ? {
eprintln('[vweb]: rejected CORS request from "${origin}". Reason: invalid header "${header}"')
}
return false
}
}
}
$if vweb_trace_cors ? {
eprintln('[vweb]: received CORS request from "${origin}": HTTP ${ctx.req.method} ${ctx.req.url}')
}
return true
}
// cors handles cross-origin requests by adding Access-Control-* headers to a
// preflight request and validating the headers of a cross-origin request.
// Example:
// ```v
// app.use(vweb.cors[Context](vweb.CorsOptions{
// origins: ['*']
// allowed_methods: [.get, .head, .patch, .put, .post, .delete]
// }))
// ```
pub fn cors[T](options CorsOptions) MiddlewareOptions[T] {
return MiddlewareOptions[T]{
handler: fn [options] [T](mut ctx T) bool {
if ctx.req.method == .options {
// preflight request
options.set_headers(mut ctx.Context)
ctx.text('ok')
return false
} else {
// check if there is a cross-origin request
if options.validate_request(mut ctx.Context) == false {
return false
}
// no cross-origin request / valid cross-origin request
return true
}
}
}
}

View file

@ -1,93 +0,0 @@
module vweb
import net.urllib
import net.http
// Parsing function attributes for methods and path.
fn parse_attrs(name string, attrs []string) !([]http.Method, string, string) {
if attrs.len == 0 {
return [http.Method.get], '/${name}', ''
}
mut x := attrs.clone()
mut methods := []http.Method{}
mut path := ''
mut host := ''
for i := 0; i < x.len; {
attr := x[i]
attru := attr.to_upper()
m := http.method_from_str(attru)
if attru == 'GET' || m != .get {
methods << m
x.delete(i)
continue
}
if attr.starts_with('/') {
if path != '' {
return http.MultiplePathAttributesError{}
}
path = attr
x.delete(i)
continue
}
if attr.starts_with('host:') {
host = attr.all_after('host:').trim_space()
x.delete(i)
continue
}
i++
}
if x.len > 0 {
return http.UnexpectedExtraAttributeError{
attributes: x
}
}
if methods.len == 0 {
methods = [http.Method.get]
}
if path == '' {
path = '/${name}'
}
// Make host lowercase for case-insensitive comparisons
return methods, path, host.to_lower()
}
fn parse_query_from_url(url urllib.URL) map[string]string {
mut query := map[string]string{}
for qvalue in url.query().data {
query[qvalue.key] = qvalue.value
}
return query
}
const boundary_start = 'boundary='
struct FileData {
pub:
filename string
content_type string
data string
}
// TODO: fix windows files? (CLRF) issues, maybe it is in the `net` module
fn parse_form_from_request(request http.Request) !(map[string]string, map[string][]http.FileData) {
if request.method !in [http.Method.post, .put, .patch] {
return map[string]string{}, map[string][]http.FileData{}
}
ct := request.header.get(.content_type) or { '' }.split(';').map(it.trim_left(' \t'))
if 'multipart/form-data' in ct {
boundaries := ct.filter(it.starts_with(boundary_start))
if boundaries.len != 1 {
return error('detected more that one form-data boundary')
}
boundary := boundaries[0].all_after(boundary_start)
if boundary.len > 0 && boundary[0] == `"` {
// quotes are send by our http.post_multipart_form/2:
return http.parse_multipart_form(request.data, boundary.trim('"'))
}
// Firefox and other browsers, do not use quotes around the boundary:
return http.parse_multipart_form(request.data, boundary)
}
return http.parse_form(request.data), map[string][]http.FileData{}
}

View file

@ -1,282 +0,0 @@
module vweb
struct RoutePair {
url string
route string
}
fn (rp RoutePair) test() ?[]string {
url := rp.url.split('/').filter(it != '')
route := rp.route.split('/').filter(it != '')
return route_matches(url, route)
}
fn (rp RoutePair) test_match() {
rp.test() or { panic('should match: ${rp}') }
}
fn (rp RoutePair) test_no_match() {
rp.test() or { return }
panic('should not match: ${rp}')
}
fn (rp RoutePair) test_param(expected []string) {
res := rp.test() or { panic('should match: ${rp}') }
assert res == expected
}
fn test_route_no_match() {
tests := [
RoutePair{
url: '/a'
route: '/a/b/c'
},
RoutePair{
url: '/a/'
route: '/a/b/c'
},
RoutePair{
url: '/a/b'
route: '/a/b/c'
},
RoutePair{
url: '/a/b/'
route: '/a/b/c'
},
RoutePair{
url: '/a/c/b'
route: '/a/b/c'
},
RoutePair{
url: '/a/c/b/'
route: '/a/b/c'
},
RoutePair{
url: '/a/b/c/d'
route: '/a/b/c'
},
RoutePair{
url: '/a/b/c'
route: '/'
},
]
for test in tests {
test.test_no_match()
}
}
fn test_route_exact_match() {
tests := [
RoutePair{
url: '/a/b/c'
route: '/a/b/c'
},
RoutePair{
url: '/a/b/c/'
route: '/a/b/c'
},
RoutePair{
url: '/a'
route: '/a'
},
RoutePair{
url: '/'
route: '/'
},
]
for test in tests {
test.test_match()
}
}
fn test_route_params_match() {
RoutePair{
url: '/a/b/c'
route: '/:a/b/c'
}.test_match()
RoutePair{
url: '/a/b/c'
route: '/a/:b/c'
}.test_match()
RoutePair{
url: '/a/b/c'
route: '/a/b/:c'
}.test_match()
RoutePair{
url: '/a/b/c'
route: '/:a/b/:c'
}.test_match()
RoutePair{
url: '/a/b/c'
route: '/:a/:b/:c'
}.test_match()
RoutePair{
url: '/one/two/three'
route: '/:a/:b/:c'
}.test_match()
RoutePair{
url: '/one/b/c'
route: '/:a/b/c'
}.test_match()
RoutePair{
url: '/one/two/three'
route: '/:a/b/c'
}.test_no_match()
RoutePair{
url: '/one/two/three'
route: '/:a/:b/c'
}.test_no_match()
RoutePair{
url: '/one/two/three'
route: '/:a/b/:c'
}.test_no_match()
RoutePair{
url: '/a/b/c/d'
route: '/:a/:b/:c'
}.test_no_match()
RoutePair{
url: '/1/2/3/4'
route: '/:a/:b/:c'
}.test_no_match()
RoutePair{
url: '/a/b'
route: '/:a/:b/:c'
}.test_no_match()
RoutePair{
url: '/1/2'
route: '/:a/:b/:c'
}.test_no_match()
}
fn test_route_params() {
RoutePair{
url: '/a/b/c'
route: '/:a/b/c'
}.test_param(['a'])
RoutePair{
url: '/one/b/c'
route: '/:a/b/c'
}.test_param(['one'])
RoutePair{
url: '/one/two/c'
route: '/:a/:b/c'
}.test_param(['one', 'two'])
RoutePair{
url: '/one/two/three'
route: '/:a/:b/:c'
}.test_param(['one', 'two', 'three'])
RoutePair{
url: '/one/b/three'
route: '/:a/b/:c'
}.test_param(['one', 'three'])
}
fn test_route_params_array_match() {
// array can only be used on the last word (TODO: add parsing / tests to ensure this)
RoutePair{
url: '/a/b/c'
route: '/a/b/:c...'
}.test_match()
RoutePair{
url: '/a/b/c/d'
route: '/a/b/:c...'
}.test_match()
RoutePair{
url: '/a/b/c/d/e'
route: '/a/b/:c...'
}.test_match()
RoutePair{
url: '/one/b/c/d/e'
route: '/:a/b/:c...'
}.test_match()
RoutePair{
url: '/one/two/c/d/e'
route: '/:a/:b/:c...'
}.test_match()
RoutePair{
url: '/one/two/three/four/five'
route: '/:a/:b/:c...'
}.test_match()
RoutePair{
url: '/a/b'
route: '/:a/:b/:c...'
}.test_no_match()
RoutePair{
url: '/a/b/'
route: '/:a/:b/:c...'
}.test_no_match()
}
fn test_route_params_array() {
RoutePair{
url: '/a/b/c'
route: '/a/b/:c...'
}.test_param(['c'])
RoutePair{
url: '/a/b/c/d'
route: '/a/b/:c...'
}.test_param(['c/d'])
RoutePair{
url: '/a/b/c/d/'
route: '/a/b/:c...'
}.test_param(['c/d'])
RoutePair{
url: '/a/b/c/d/e'
route: '/a/b/:c...'
}.test_param(['c/d/e'])
RoutePair{
url: '/one/b/c/d/e'
route: '/:a/b/:c...'
}.test_param(['one', 'c/d/e'])
RoutePair{
url: '/one/two/c/d/e'
route: '/:a/:b/:c...'
}.test_param(['one', 'two', 'c/d/e'])
RoutePair{
url: '/one/two/three/d/e'
route: '/:a/:b/:c...'
}.test_param(['one', 'two', 'three/d/e'])
}
fn test_route_index_path() {
RoutePair{
url: '/'
route: '/:path...'
}.test_param(['/'])
RoutePair{
url: '/foo/bar'
route: '/:path...'
}.test_param(['/foo/bar'])
}

View file

@ -1,12 +0,0 @@
module vweb
fn C.sendfile(in_fd int, out_fd int, offset int, count int, voidptr offsetp, voidptr hdr, flags int) int
fn sendfile(out_fd int, in_fd int, nr_bytes int) int {
// out_fd must be a stream socket descriptor.
r := C.sendfile(in_fd, out_fd, 0, nr_bytes, unsafe { nil }, unsafe { nil }, 0)
if r == 0 {
return nr_bytes
}
return r
}

View file

@ -1,10 +0,0 @@
module vweb
#include <sys/sendfile.h>
fn C.sendfile(out_fd int, in_fd int, offset voidptr, count int) int
fn sendfile(out_fd int, in_fd int, nr_bytes int) int {
// always pass nil as offset, so the file offset will be used and updated.
return C.sendfile(out_fd, in_fd, 0, nr_bytes)
}

View file

@ -1,63 +0,0 @@
# Server Sent Events
This module implements the server side of `Server Sent Events`, SSE.
See [mozilla SSE][mozilla_sse]
as well as [whatwg][whatwg html spec]
for detailed description of the protocol, and a simple web browser client example.
## Usage
With SSE we want to keep the connection open, so we are able to
keep sending events to the client. But if we hold the connection open indefinitely
vweb isn't able to process any other requests.
We can let vweb know that it can continue processing other requests and that we will
handle the connection ourself by calling `ctx.takeover_conn()` and returning an empty result
with `vweb.no_result()`. Vweb will not close the connection and we can handle
the connection in a separate thread.
**Example:**
```v ignore
import x.vweb.sse
// endpoint handler for SSE connections
fn (app &App) sse(mut ctx Context) vweb.Result {
// let vweb know that the connection should not be closed
ctx.takeover_conn()
// handle the connection in a new thread
spawn handle_sse_conn(mut ctx)
// we will send a custom response ourself, so we can safely return an empty result
return vweb.no_result()
}
fn handle_sse_conn(mut ctx Context) {
// pass vweb.Context
mut sse_conn := sse.start_connection(mut ctx.Context)
// send a message every second 3 times
for _ in 0.. 3 {
time.sleep(time.second)
sse_conn.send_message(data: 'ping') or { break }
}
// close the SSE connection
sse_conn.close()
}
```
Javascript code:
```js
const eventSource = new EventSource('/sse');
eventSource.addEventListener('message', (event) => {
console.log('received message:', event.data);
});
eventSource.addEventListener('close', () => {
console.log('closing the connection')
// prevent browser from reconnecting
eventSource.close();
});
```
[mozilla_sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
[whatwg html spec]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events

View file

@ -1,73 +0,0 @@
module sse
import x.vweb
import net
import strings
// This module implements the server side of `Server Sent Events`.
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
// as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
// for detailed description of the protocol, and a simple web browser client example.
//
// > Event stream format
// > The event stream is a simple stream of text data which must be encoded using UTF-8.
// > Messages in the event stream are separated by a pair of newline characters.
// > A colon as the first character of a line is in essence a comment, and is ignored.
// > Note: The comment line can be used to prevent connections from timing out;
// > a server can send a comment periodically to keep the connection alive.
// >
// > Each message consists of one or more lines of text listing the fields for that message.
// > Each field is represented by the field name, followed by a colon, followed by the text
// > data for that field's value.
@[params]
pub struct SSEMessage {
pub mut:
id string
event string
data string
retry int
}
@[heap]
pub struct SSEConnection {
pub mut:
conn &net.TcpConn @[required]
}
// start an SSE connection
pub fn start_connection(mut ctx vweb.Context) &SSEConnection {
ctx.res.header.set(.connection, 'keep-alive')
ctx.res.header.set(.cache_control, 'no-cache')
ctx.send_response_to_client('text/event-stream', '')
return &SSEConnection{
conn: ctx.conn
}
}
// send_message sends a single message to the http client that listens for SSE.
// It does not close the connection, so you can use it many times in a loop.
pub fn (mut sse SSEConnection) send_message(message SSEMessage) ! {
mut sb := strings.new_builder(512)
if message.id != '' {
sb.write_string('id: ${message.id}\n')
}
if message.event != '' {
sb.write_string('event: ${message.event}\n')
}
if message.data != '' {
sb.write_string('data: ${message.data}\n')
}
if message.retry != 0 {
sb.write_string('retry: ${message.retry}\n')
}
sb.write_string('\n')
sse.conn.write(sb)!
}
// send a 'close' event and close the tcp connection.
pub fn (mut sse SSEConnection) close() {
sse.send_message(event: 'close', data: 'Closing the connection', retry: -1) or {}
sse.conn.close() or {}
}

View file

@ -1,74 +0,0 @@
// vtest retry: 3
import x.vweb
import x.vweb.sse
import time
import net.http
const port = 23008
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
}
pub struct App {
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
fn (app &App) sse(mut ctx Context) vweb.Result {
ctx.takeover_conn()
spawn handle_sse_conn(mut ctx)
return vweb.no_result()
}
fn handle_sse_conn(mut ctx Context) {
mut sse_conn := sse.start_connection(mut ctx.Context)
for _ in 0 .. 3 {
time.sleep(time.second)
sse_conn.send_message(data: 'ping') or { break }
}
sse_conn.close()
}
fn testsuite_begin() {
mut app := &App{}
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
spawn vweb.run_at[App, Context](mut app, port: port, family: .ip)
// app startup time
_ := <-app.started
}
fn test_sse() ! {
mut x := http.get('${localserver}/sse')!
connection := x.header.get(.connection) or {
assert true == false, 'Header Connection should be set!'
panic('missing header')
}
cache_control := x.header.get(.cache_control) or {
assert true == false, 'Header Cache-Control should be set!'
panic('missing header')
}
content_type := x.header.get(.content_type) or {
assert true == false, 'Header Content-Type should be set!'
panic('missing header')
}
assert connection == 'keep-alive'
assert cache_control == 'no-cache'
assert content_type == 'text/event-stream'
eprintln(x.body)
assert x.body == 'data: ping\n\ndata: ping\n\ndata: ping\n\nevent: close\ndata: Closing the connection\nretry: -1\n\n'
}

View file

@ -1,115 +0,0 @@
module vweb
import os
pub interface StaticApp {
mut:
static_files map[string]string
static_mime_types map[string]string
static_hosts map[string]string
}
// StaticHandler provides methods to handle static files in your vweb App
pub struct StaticHandler {
pub mut:
static_files map[string]string
static_mime_types map[string]string
static_hosts map[string]string
}
// scan_static_directory recursively scans `directory_path` and returns an error if
// no valid MIME type can be found
fn (mut sh StaticHandler) scan_static_directory(directory_path string, mount_path string, host string) ! {
files := os.ls(directory_path) or { panic(err) }
if files.len > 0 {
for file in files {
full_path := os.join_path(directory_path, file)
if os.is_dir(full_path) {
sh.scan_static_directory(full_path, mount_path.trim_right('/') + '/' + file,
host)!
} else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') {
sh.host_serve_static(host, mount_path.trim_right('/') + '/' + file, full_path)!
}
}
}
}
// handle_static is used to mark a folder (relative to the current working folder)
// as one that contains only static resources (css files, images etc).
// If `root` is set the mount path for the dir will be in '/'
// Usage:
// ```v
// os.chdir( os.executable() )?
// app.handle_static('assets', true)
// ```
pub fn (mut sh StaticHandler) handle_static(directory_path string, root bool) !bool {
return sh.host_handle_static('', directory_path, root)!
}
// host_handle_static is used to mark a folder (relative to the current working folder)
// as one that contains only static resources (css files, images etc).
// If `root` is set the mount path for the dir will be in '/'
// Usage:
// ```v
// os.chdir( os.executable() )?
// app.host_handle_static('localhost', 'assets', true)
// ```
pub fn (mut sh StaticHandler) host_handle_static(host string, directory_path string, root bool) !bool {
if !os.exists(directory_path) {
return error('directory `${directory_path}` does not exist. The directory should be relative to the current working directory: ${os.getwd()}')
}
dir_path := directory_path.trim_space().trim_right('/')
mut mount_path := ''
if dir_path != '.' && os.is_dir(dir_path) && !root {
// Mount point hygiene, "./assets" => "/assets".
mount_path = '/' + dir_path.trim_left('.').trim('/')
}
sh.scan_static_directory(dir_path, mount_path, host)!
return true
}
// mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path
// For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
// and you have a file /var/share/myassets/main.css .
// => That file will be available at URL: http://server/assets/main.css .
pub fn (mut sh StaticHandler) mount_static_folder_at(directory_path string, mount_path string) !bool {
return sh.host_mount_static_folder_at('', directory_path, mount_path)!
}
// host_mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://host/mount_path
// For example: suppose you have called .host_mount_static_folder_at('localhost', '/var/share/myassets', '/assets'),
// and you have a file /var/share/myassets/main.css .
// => That file will be available at URL: http://localhost/assets/main.css .
pub fn (mut sh StaticHandler) host_mount_static_folder_at(host string, directory_path string, mount_path string) !bool {
if mount_path == '' || mount_path[0] != `/` {
return error('invalid mount path! The path should start with `/`')
} else if !os.exists(directory_path) {
return error('directory `${directory_path}` does not exist. The directory should be relative to the current working directory: ${os.getwd()}')
}
dir_path := directory_path.trim_right('/')
trim_mount_path := mount_path.trim_left('/').trim_right('/')
sh.scan_static_directory(dir_path, '/${trim_mount_path}', host)!
return true
}
// Serves a file static
// `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type
pub fn (mut sh StaticHandler) serve_static(url string, file_path string) ! {
sh.host_serve_static('', url, file_path)!
}
// Serves a file static
// `url` is the access path on the site, `file_path` is the real path to the file
// `host` is the host to serve the file from
pub fn (mut sh StaticHandler) host_serve_static(host string, url string, file_path string) ! {
ext := os.file_ext(file_path).to_lower()
// Rudimentary guard against adding files not in mime_types.
if ext !in sh.static_mime_types && ext !in mime_types {
return error('unknown MIME type for file extension "${ext}". You can register your MIME type in `app.static_mime_types`')
}
sh.static_files[url] = file_path
sh.static_hosts[url] = host
}

View file

@ -1,131 +0,0 @@
import x.vweb
import time
import os
import net.http
const port = 23006
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
}
pub struct App {
vweb.Controller
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('from app')
}
@['/conflict/test']
pub fn (app &App) conflicting(mut ctx Context) vweb.Result {
return ctx.text('from conflicting')
}
pub struct Other {
vweb.Controller
}
pub fn (app &Other) index(mut ctx Context) vweb.Result {
return ctx.text('from other')
}
pub struct HiddenByOther {}
pub fn (app &HiddenByOther) index(mut ctx Context) vweb.Result {
return ctx.text('from hidden')
}
pub struct SubController {}
pub fn (app &SubController) index(mut ctx Context) vweb.Result {
return ctx.text('from sub')
}
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!
mut sub := &SubController{}
mut other := &Other{}
other.register_controller[SubController, Context]('/sub', mut sub)!
mut hidden := &HiddenByOther{}
mut app := &App{}
app.register_controller[Other, Context]('/other', mut other)!
// controllers should be sorted, so this controller should be accessible
// even though it is declared last
app.register_controller[HiddenByOther, Context]('/other/hide', mut hidden)!
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
_ := <-app.started
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
}
fn test_app_home() {
x := http.get(localserver)!
assert x.body == 'from app'
}
fn test_other() {
x := http.get('${localserver}/other')!
assert x.body == 'from other'
}
fn test_sub_controller() {
x := http.get('${localserver}/other/sub')!
assert x.body == 'from sub'
}
fn test_hidden_route() {
x := http.get('${localserver}/other/hide')!
assert x.body == 'from hidden'
}
fn test_conflicting_controllers() {
mut other := &Other{}
mut app := &App{}
app.register_controller[Other, Context]('/other', mut other) or {
assert true == false, 'this should not fail'
}
app.register_controller[Other, Context]('/other', mut other) or {
assert true == false, 'this should not fail'
}
vweb.run_at[App, Context](mut app, port: port) or {
assert err.msg() == 'conflicting paths: duplicate controller handling the route "/other"'
return
}
assert true == false, 'the previous call should have failed!'
}
fn test_conflicting_controller_routes() {
mut other := &Other{}
mut app := &App{}
app.register_controller[Other, Context]('/conflict', mut other) or {
assert true == false, 'this should not fail'
}
vweb.run_at[App, Context](mut app, port: port) or {
assert err.msg() == 'conflicting paths: method "conflicting" with route "/conflict/test" should be handled by the Controller of path "/conflict"'
return
}
assert true == false, 'the previous call should have failed!'
}

View file

@ -1,107 +0,0 @@
import x.vweb
import net.http
import os
import time
const port = 23012
const localserver = 'http://localhost:${port}'
const exit_after = time.second * 10
const allowed_origin = 'https://vlang.io'
const cors_options = vweb.CorsOptions{
origins: [allowed_origin]
allowed_methods: [.get, .head]
}
pub struct Context {
vweb.Context
}
pub struct App {
vweb.Middleware[Context]
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('index')
}
@[post]
pub fn (app &App) post(mut ctx Context) vweb.Result {
return ctx.text('post')
}
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
mut app := &App{}
app.use(vweb.cors[Context](cors_options))
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
}
fn test_valid_cors() {
x := http.fetch(http.FetchConfig{
url: localserver
method: .get
header: http.new_header_from_map({
.origin: allowed_origin
})
})!
assert x.status() == .ok
assert x.body == 'index'
}
fn test_preflight() {
x := http.fetch(http.FetchConfig{
url: localserver
method: .options
header: http.new_header_from_map({
.origin: allowed_origin
})
})!
assert x.status() == .ok
assert x.body == 'ok'
assert x.header.get(.access_control_allow_origin)! == allowed_origin
if _ := x.header.get(.access_control_allow_credentials) {
assert false, 'Access-Control-Allow-Credentials should not be present the value is `false`'
}
assert x.header.get(.access_control_allow_methods)! == 'GET, HEAD'
}
fn test_invalid_origin() {
x := http.fetch(http.FetchConfig{
url: localserver
method: .get
header: http.new_header_from_map({
.origin: 'https://google.com'
})
})!
assert x.status() == .forbidden
}
fn test_invalid_method() {
x := http.fetch(http.FetchConfig{
url: '${localserver}/post'
method: .post
header: http.new_header_from_map({
.origin: allowed_origin
})
})!
assert x.status() == .method_not_allowed
}

View file

@ -1,126 +0,0 @@
// vtest flaky: true
// vtest retry: 3
import x.vweb
import net.http
import time
import os
const port = 23002
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
const tmp_file = os.join_path(os.vtmp_dir(), 'vweb_large_payload.txt')
pub struct App {
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
return ctx.text('Hello V!')
}
@[post]
pub fn (mut app App) post_request(mut ctx Context) vweb.Result {
return ctx.text(ctx.req.data)
}
pub fn (app &App) file(mut ctx Context) vweb.Result {
return ctx.file(tmp_file)
}
pub struct Context {
vweb.Context
}
fn testsuite_begin() {
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
mut app := &App{}
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started
}
fn test_large_request_body() {
// string of a's of 8.96mb send over the connection
// vweb reads a maximum of 4096KB per picoev loop cycle
// this test tests if vweb is able to do multiple of these
// cycles and updates the response body each cycle
mut buf := []u8{len: vweb.max_read * 10, init: `a`}
str := buf.bytestr()
mut x := http.post('${localserver}/post_request', str)!
assert x.body.len == vweb.max_read * 10
}
fn test_large_request_header() {
// same test as test_large_request_body, but then with a large header,
// which is parsed separately
mut buf := []u8{len: vweb.max_read * 2, init: `a`}
str := buf.bytestr()
// make 1 header longer than vwebs max read limit
mut x := http.fetch(http.FetchConfig{
url: localserver
header: http.new_custom_header_from_map({
'X-Overflow-Header': str
})!
})!
assert x.status() == .request_entity_too_large
}
fn test_bigger_content_length() {
data := '123456789'
mut x := http.fetch(http.FetchConfig{
method: .post
url: '${localserver}/post_request'
header: http.new_header_from_map({
.content_length: '10'
})
data: data
})!
// Content-length is larger than the data sent, so the request should timeout
assert x.status() == .request_timeout
}
fn test_smaller_content_length() {
data := '123456789'
mut x := http.fetch(http.FetchConfig{
method: .post
url: '${localserver}/post_request'
header: http.new_header_from_map({
.content_length: '5'
})
data: data
})!
assert x.status() == .bad_request
assert x.body == 'Mismatch of body length and Content-Length header'
}
fn test_sendfile() {
mut buf := []u8{len: vweb.max_write * 10, init: `a`}
os.write_file(tmp_file, buf.bytestr())!
x := http.get('${localserver}/file')!
assert x.body.len == vweb.max_write * 10
}
fn testsuite_end() {
os.rm(tmp_file)!
}

View file

@ -1,129 +0,0 @@
import x.vweb
import net.http
import os
import time
const port = 23001
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
pub mut:
counter int
}
@[heap]
pub struct App {
vweb.Middleware[Context]
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
return ctx.text('from index, ${ctx.counter}')
}
@['/bar/bar']
pub fn (app &App) bar(mut ctx Context) vweb.Result {
return ctx.text('from bar, ${ctx.counter}')
}
pub fn (app &App) unreachable(mut ctx Context) vweb.Result {
return ctx.text('should never be reachable!')
}
@['/nested/route/method']
pub fn (app &App) nested(mut ctx Context) vweb.Result {
return ctx.text('from nested, ${ctx.counter}')
}
pub fn (app &App) after(mut ctx Context) vweb.Result {
return ctx.text('from after, ${ctx.counter}')
}
pub fn (app &App) app_middleware(mut ctx Context) bool {
ctx.counter++
return true
}
fn middleware_handler(mut ctx Context) bool {
ctx.counter++
return true
}
fn middleware_unreachable(mut ctx Context) bool {
ctx.text('unreachable, ${ctx.counter}')
return false
}
fn after_middleware(mut ctx Context) bool {
ctx.counter++
ctx.res.header.add_custom('X-AFTER', ctx.counter.str()) or { panic('bad') }
return true
}
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!
mut app := &App{}
// even though `route_use` is called first, global middleware is still executed first
app.Middleware.route_use('/unreachable', handler: middleware_unreachable)
// global middleware
app.Middleware.use(handler: middleware_handler)
app.Middleware.use(handler: app.app_middleware)
// should match only one slash
app.Middleware.route_use('/bar/:foo', handler: middleware_handler)
// should match multiple slashes
app.Middleware.route_use('/nested/:path...', handler: middleware_handler)
app.Middleware.route_use('/after', handler: after_middleware, after: true)
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
}
fn test_index() {
x := http.get(localserver)!
assert x.body == 'from index, 2'
}
fn test_unreachable_order() {
x := http.get('${localserver}/unreachable')!
assert x.body == 'unreachable, 2'
}
fn test_dynamic_route() {
x := http.get('${localserver}/bar/bar')!
assert x.body == 'from bar, 3'
}
fn test_nested() {
x := http.get('${localserver}/nested/route/method')!
assert x.body == 'from nested, 3'
}
fn test_after_middleware() {
x := http.get('${localserver}/after')!
assert x.body == 'from after, 2'
custom_header := x.header.get_custom('X-AFTER') or { panic('should be set!') }
assert custom_header == '3'
}
// TODO: add test for encode and decode gzip

View file

@ -1,126 +0,0 @@
import net
import net.http
import io
import os
import time
import x.vweb
const exit_after = time.second * 10
const port = 23009
const localserver = 'localhost:${port}'
const tcp_r_timeout = 2 * time.second
const tcp_w_timeout = 2 * time.second
const max_retries = 4
const default_request = 'GET / HTTP/1.1
User-Agent: VTESTS
Accept: */*
\r\n'
const response_body = 'intact!'
pub struct Context {
vweb.Context
}
pub struct App {
mut:
started chan bool
counter int
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
app.counter++
return ctx.text('${response_body}:${app.counter}')
}
pub fn (mut app App) reset(mut ctx Context) vweb.Result {
app.counter = 0
return ctx.ok('')
}
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!
mut app := &App{}
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 5)
_ := <-app.started
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
}
fn test_conn_remains_intact() {
http.get('http://${localserver}/reset')!
mut conn := simple_tcp_client()!
conn.write_string(default_request)!
mut read := io.read_all(reader: conn)!
mut response := read.bytestr()
assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
assert response.ends_with('${response_body}:1') == true, 'read response: ${response}'
// send request again over the same connection
conn.write_string(default_request)!
read = io.read_all(reader: conn)!
response = read.bytestr()
assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
assert response.ends_with('${response_body}:2') == true, 'read response: ${response}'
conn.close() or {}
}
fn test_support_http_1() {
http.get('http://${localserver}/reset')!
// HTTP 1.0 always closes the connection after each request, so the client must
// send the Connection: close header. If that header is present the connection
// needs to be closed and a `Connection: close` header needs to be send back
mut x := http.fetch(http.FetchConfig{
url: 'http://${localserver}/'
header: http.new_header_from_map({
.connection: 'close'
})
})!
assert x.status() == .ok
if conn_header := x.header.get(.connection) {
assert conn_header == 'close'
} else {
assert false, '`Connection: close` header should be present!'
}
}
// utility code:
fn simple_tcp_client() !&net.TcpConn {
mut client := &net.TcpConn(unsafe { nil })
mut tries := 0
for tries < max_retries {
tries++
eprintln('> client retries: ${tries}')
client = net.dial_tcp(localserver) or {
eprintln('dial error: ${err.msg()}')
if tries > max_retries {
return err
}
time.sleep(100 * time.millisecond)
continue
}
break
}
if client == unsafe { nil } {
eprintln('could not create a tcp client connection to http://${localserver} after ${max_retries} retries')
exit(1)
}
client.set_read_timeout(tcp_r_timeout)
client.set_write_timeout(tcp_w_timeout)
return client
}

View file

@ -1,128 +0,0 @@
import x.vweb
import net.http
import os
import time
const port = 23003
const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct App {
vweb.StaticHandler
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
return ctx.text('Hello V!')
}
@[post]
pub fn (mut app App) post_request(mut ctx Context) vweb.Result {
return ctx.text(ctx.req.data)
}
pub struct Context {
vweb.Context
}
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()
run_app_test()
}
fn run_app_test() {
mut app := &App{}
if _ := app.handle_static('testdata', true) {
assert true == false, 'should throw unknown mime type error'
} else {
assert err.msg().starts_with('unknown MIME type for file extension ".what"'), 'throws error on unknown mime type'
}
app.static_mime_types['.what'] = vweb.mime_types['.txt']
if _ := app.handle_static('not_found', true) {
assert false, 'should throw directory not found error'
} else {
assert err.msg().starts_with('directory `not_found` does not exist') == true
}
app.handle_static('testdata', true) or { panic(err) }
if _ := app.mount_static_folder_at('testdata', 'static') {
assert true == false, 'should throw invalid mount path error'
} else {
assert err.msg() == 'invalid mount path! The path should start with `/`'
}
if _ := app.mount_static_folder_at('not_found', '/static') {
assert true == false, 'should throw mount path does not exist error'
} else {
assert err.msg().starts_with('directory `not_found` does not exist') == true
}
app.mount_static_folder_at('testdata', '/static') or { panic(err) }
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started
}
fn test_static_root() {
x := http.get('${localserver}/root.txt')!
assert x.status() == .ok
assert x.body == 'root'
}
fn test_scans_subdirs() {
x := http.get('${localserver}/sub_folder/sub.txt')!
assert x.status() == .ok
assert x.body == 'sub'
}
fn test_index_subdirs() {
x := http.get('${localserver}/sub_folder/')!
y := http.get('${localserver}/sub.folder/sub_folder')!
assert x.status() == .ok
assert x.body.trim_space() == 'OK'
assert y.status() == .ok
assert y.body.trim_space() == 'OK'
}
fn test_custom_mime_types() {
x := http.get('${localserver}/unknown_mime.what')!
assert x.status() == .ok
assert x.header.get(.content_type)! == vweb.mime_types['.txt']
assert x.body.trim_space() == 'unknown_mime'
}
fn test_custom_folder_mount() {
x := http.get('${localserver}/static/root.txt')!
assert x.status() == .ok
assert x.body == 'root'
}
fn test_upper_case_mime_type() {
x := http.get('${localserver}/upper_case.TXT')!
assert x.status() == .ok
assert x.body == 'body'
}

View file

@ -1 +0,0 @@
root

View file

@ -1 +0,0 @@
OK

View file

@ -1 +0,0 @@
sub

View file

@ -1 +0,0 @@
OK

View file

@ -1 +0,0 @@
sub

View file

@ -1 +0,0 @@
unknown_mime

View file

@ -1 +0,0 @@
body

View file

@ -1,121 +0,0 @@
import x.vweb
import time
import db.sqlite
const port = 23004
pub struct Context {
vweb.Context
pub mut:
user_id string
}
pub struct App {
pub mut:
db sqlite.DB
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
struct Article {
id int
title string
text string
}
fn test_a_vweb_application_compiles() {
spawn fn () {
time.sleep(15 * time.second)
exit(0)
}()
mut app := &App{}
spawn vweb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
}
pub fn (mut ctx Context) before_request() {
ctx.user_id = ctx.get_cookie('id') or { '0' }
}
@['/new_article'; post]
pub fn (mut app App) new_article(mut ctx Context) vweb.Result {
title := ctx.form['title']
text := ctx.form['text']
if title == '' || text == '' {
return ctx.text('Empty text/title')
}
article := Article{
title: title
text: text
}
println('posting article')
println(article)
sql app.db {
insert article into Article
} or {}
return ctx.redirect('/', typ: .see_other)
}
pub fn (mut app App) time(mut ctx Context) vweb.Result {
return ctx.text(time.now().format())
}
pub fn (mut app App) time_json(mut ctx Context) vweb.Result {
return ctx.json({
'time': time.now().format()
})
}
fn (mut app App) time_json_pretty(mut ctx Context) vweb.Result {
return ctx.json_pretty({
'time': time.now().format()
})
}
struct ApiSuccessResponse[T] {
success bool
result T
}
fn (mut app App) json_success[T](mut ctx Context, result T) {
response := ApiSuccessResponse[T]{
success: true
result: result
}
ctx.json(response)
}
// should compile, this is a helper method, not exposed as a route
fn (mut app App) some_helper[T](result T) ApiSuccessResponse[T] {
response := ApiSuccessResponse[T]{
success: true
result: result
}
return response
}
// should compile, the route method itself is not generic
fn (mut app App) ok(mut ctx Context) vweb.Result {
return ctx.json(app.some_helper(123))
}
struct ExampleStruct {
example int
}
fn (mut app App) request_raw_2(mut ctx Context) vweb.Result {
stuff := []ExampleStruct{}
app.request_raw(mut ctx, stuff)
return ctx.ok('')
}
// should compile, this is a helper method, not exposed as a route
fn (mut app App) request_raw(mut ctx Context, foo []ExampleStruct) {
ctx.text('Hello world')
}

View file

@ -1,123 +0,0 @@
import os
import log
import time
import x.vweb
import net.http
const vexe = os.getenv('VEXE')
const vroot = os.dir(vexe)
const port = 23013
const welcome_text = 'Welcome to our simple vweb server'
// Use a known good http client like `curl` (if it exists):
const curl_executable = os.find_abs_path_of_executable('curl') or { '' }
const curl_ok = curl_supports_ipv6()
fn curl_supports_ipv6() bool {
if curl_executable == '' {
return false
}
curl_res := os.execute('${curl_executable} --version')
if curl_res.exit_code != 0 {
return false
}
if !curl_res.output.match_glob('curl*Features: * IPv6 *') {
return false
}
return true
}
fn testsuite_begin() {
log.set_level(.debug)
log.debug(@FN)
os.chdir(vroot) or {}
if curl_ok {
log.info('working curl_executable found at: ${curl_executable}')
} else {
log.warn('no working working curl_executable found')
}
start_services()
}
fn testsuite_end() {
log.debug(@FN)
}
//
fn ensure_curl_works(tname string) ? {
if !curl_ok {
log.warn('skipping test ${tname}, since it needs a working curl')
return none
}
}
fn test_curl_connecting_through_ipv4_works() {
ensure_curl_works(@FN) or { return }
res := os.execute('${curl_executable} --connect-timeout 0.5 --silent http://127.0.0.1:${port}/')
assert res.exit_code == 0, res.output
assert res.output == welcome_text
log.info('> ${@FN}')
}
fn test_net_http_connecting_through_ipv4_works() {
res := http.get('http://127.0.0.1:${port}/')!
assert res.status_code == 200, res.str()
assert res.status_msg == 'OK', res.str()
assert res.body == welcome_text, res.str()
log.info('> ${@FN}')
}
fn test_curl_connecting_through_ipv6_works() {
ensure_curl_works(@FN) or { return }
res := os.execute('${curl_executable} --silent --connect-timeout 0.5 http://[::1]:${port}/')
assert res.exit_code == 0, res.output
assert res.output == welcome_text
log.info('> ${@FN}')
}
fn test_net_http_connecting_through_ipv6_works() {
$if windows {
log.warn('skipping test ${@FN} on windows for now')
return
}
res := http.get('http://[::1]:${port}/')!
assert res.status_code == 200, res.str()
assert res.status_msg == 'OK', res.str()
assert res.body == welcome_text, res.str()
log.info('> ${@FN}')
}
//
pub struct Context {
vweb.Context
}
pub struct App {
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
return ctx.text(welcome_text)
}
fn start_services() {
log.debug('starting watchdog thread to ensure the test will always exit one way or another...')
spawn fn (timeout_in_ms int) {
time.sleep(timeout_in_ms * time.millisecond)
log.error('Timeout of ${timeout_in_ms} ms reached, for webserver: pid: ${os.getpid()}. Exiting ...')
exit(1)
}(10_000)
log.debug('starting webserver...')
mut app := &App{}
spawn vweb.run[App, Context](mut app, port)
_ := <-app.started
log.debug('webserver started')
}

View file

@ -1,361 +0,0 @@
import os
import time
import json
import net
import net.http
import io
const sport = 13005
const localserver = '127.0.0.1:${sport}'
const exit_after_time = 12000
// milliseconds
const vexe = os.getenv('VEXE')
const vweb_logfile = os.getenv('VWEB_LOGFILE')
const vroot = os.dir(vexe)
const serverexe = os.join_path(os.cache_dir(), 'vweb_test_server.exe')
const tcp_r_timeout = 10 * time.second
const tcp_w_timeout = 10 * time.second
// setup of vweb webserver
fn testsuite_begin() {
os.chdir(vroot) or {}
if os.exists(serverexe) {
os.rm(serverexe) or {}
}
}
fn test_a_simple_vweb_app_can_be_compiled() {
// did_server_compile := os.system('${os.quoted_path(vexe)} -g -o ${os.quoted_path(serverexe)} vlib/vweb/tests/vweb_test_server.v')
did_server_compile := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(serverexe)} vlib/x/vweb/tests/vweb_test_server.v')
assert did_server_compile == 0
assert os.exists(serverexe)
}
fn test_a_simple_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)
}
}
// web client tests follow
fn assert_common_headers(received string) {
assert received.starts_with('HTTP/1.1 200 OK\r\n')
assert received.contains('Server: VWeb\r\n')
assert received.contains('Content-Length:')
assert received.contains('Connection: close\r\n')
}
fn test_a_simple_tcp_client_can_connect_to_the_vweb_server() {
received := simple_tcp_client(path: '/') or {
assert err.msg() == ''
return
}
assert_common_headers(received)
assert received.contains('Content-Type: text/plain')
assert received.contains('Content-Length: 15')
assert received.ends_with('Welcome to VWeb')
}
fn test_a_simple_tcp_client_simple_route() {
received := simple_tcp_client(path: '/simple') or {
assert err.msg() == ''
return
}
assert_common_headers(received)
assert received.contains('Content-Type: text/plain')
assert received.contains('Content-Length: 15')
assert received.ends_with('A simple result')
}
fn test_a_simple_tcp_client_zero_content_length() {
// tests that sending a content-length header of 0 doesn't hang on a read timeout
watch := time.new_stopwatch(auto_start: true)
simple_tcp_client(path: '/', headers: 'Content-Length: 0\r\n\r\n') or {
assert err.msg() == ''
return
}
assert watch.elapsed() < 1 * time.second
}
fn test_a_simple_tcp_client_html_page() {
received := simple_tcp_client(path: '/html_page') or {
assert err.msg() == ''
return
}
assert_common_headers(received)
assert received.contains('Content-Type: text/html')
assert received.ends_with('<h1>ok</h1>')
}
// net.http client based tests follow:
fn assert_common_http_headers(x http.Response) ! {
assert x.status() == .ok
assert x.header.get(.server)! == 'VWeb'
assert x.header.get(.content_length)!.int() > 0
}
fn test_http_client_index() {
x := http.get('http://${localserver}/') or { panic(err) }
assert_common_http_headers(x)!
assert x.header.get(.content_type)! == 'text/plain'
assert x.body == 'Welcome to VWeb'
assert x.header.get(.connection)! == 'close'
}
fn test_http_client_404() {
server := 'http://${localserver}'
url_404_list := [
'/zxcnbnm',
'/JHKAJA',
'/unknown',
]
for url in url_404_list {
res := http.get('${server}${url}') or { panic(err) }
assert res.status() == .not_found
assert res.body == '404 on "${url}"'
}
}
fn test_http_client_simple() {
x := http.get('http://${localserver}/simple') or { panic(err) }
assert_common_http_headers(x)!
assert x.header.get(.content_type)! == 'text/plain'
assert x.body == 'A simple result'
}
fn test_http_client_html_page() {
x := http.get('http://${localserver}/html_page') or { panic(err) }
assert_common_http_headers(x)!
assert x.header.get(.content_type)! == 'text/html'
assert x.body == '<h1>ok</h1>'
}
fn test_http_client_settings_page() {
x := http.get('http://${localserver}/bilbo/settings') or { panic(err) }
assert_common_http_headers(x)!
assert x.body == 'username: bilbo'
y := http.get('http://${localserver}/kent/settings') or { panic(err) }
assert_common_http_headers(y)!
assert y.body == 'username: kent'
}
fn test_http_client_user_repo_settings_page() {
x := http.get('http://${localserver}/bilbo/gostamp/settings') or { panic(err) }
assert_common_http_headers(x)!
assert x.body == 'username: bilbo | repository: gostamp'
y := http.get('http://${localserver}/kent/golang/settings') or { panic(err) }
assert_common_http_headers(y)!
assert y.body == 'username: kent | repository: golang'
z := http.get('http://${localserver}/missing/golang/settings') or { panic(err) }
assert z.status() == .not_found
}
struct User {
name string
age int
}
fn test_http_client_json_post() {
ouser := User{
name: 'Bilbo'
age: 123
}
json_for_ouser := json.encode(ouser)
mut x := http.post_json('http://${localserver}/json_echo', json_for_ouser) or { panic(err) }
$if debug_net_socket_client ? {
eprintln('/json_echo endpoint response: ${x}')
}
assert x.header.get(.content_type)! == 'application/json'
assert x.body == json_for_ouser
nuser := json.decode(User, x.body) or { User{} }
assert '${ouser}' == '${nuser}'
x = http.post_json('http://${localserver}/json', json_for_ouser) or { panic(err) }
$if debug_net_socket_client ? {
eprintln('/json endpoint response: ${x}')
}
assert x.header.get(.content_type)! == 'application/json'
assert x.body == json_for_ouser
nuser2 := json.decode(User, x.body) or { User{} }
assert '${ouser}' == '${nuser2}'
}
fn test_http_client_multipart_form_data() {
mut form_config := http.PostMultipartFormConfig{
form: {
'foo': 'baz buzz'
}
}
mut x := http.post_multipart_form('http://${localserver}/form_echo', form_config)!
$if debug_net_socket_client ? {
eprintln('/form_echo endpoint response: ${x}')
}
assert x.body == form_config.form['foo']
mut files := []http.FileData{}
files << http.FileData{
filename: 'vweb'
content_type: 'text'
data: '"vweb test"'
}
mut form_config_files := http.PostMultipartFormConfig{
files: {
'file': files
}
}
x = http.post_multipart_form('http://${localserver}/file_echo', form_config_files)!
$if debug_net_socket_client ? {
eprintln('/form_echo endpoint response: ${x}')
}
assert x.body == files[0].data
}
fn test_login_with_multipart_form_data_send_by_fetch() {
mut form_config := http.PostMultipartFormConfig{
form: {
'username': 'myusername'
'password': 'mypassword123'
}
}
x := http.post_multipart_form('http://${localserver}/login', form_config)!
assert x.status_code == 200
assert x.status_msg == 'OK'
assert x.body == 'username: xmyusernamex | password: xmypassword123x'
}
fn test_query_params_are_passed_as_arguments() {
x := http.get('http://${localserver}/query_echo?c=3&a="test"&b=20')!
assert x.status() == .ok
assert x.body == 'a: x"test"x | b: x20x'
}
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() == ''
return
}
assert x.status() == .not_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 = 4
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(unsafe { nil })
mut tries := 0
for tries < config.retries {
tries++
eprintln('> client retries: ${tries}')
client = net.dial_tcp(localserver) or {
eprintln('dial error: ${err.msg()}')
if tries > config.retries {
return err
}
time.sleep(100 * time.millisecond)
continue
}
break
}
if client == unsafe { nil } {
eprintln('could not create a tcp client connection to http://${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: */*
Connection: close
${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()
}
// for issue 20476
// phenomenon: parsing url error when querypath is `//`
fn test_empty_querypath() {
mut x := http.get('http://${localserver}') or { panic(err) }
assert x.body == 'Welcome to VWeb'
x = http.get('http://${localserver}/') or { panic(err) }
assert x.body == 'Welcome to VWeb'
x = http.get('http://${localserver}//') or { panic(err) }
assert x.body == 'Welcome to VWeb'
x = http.get('http://${localserver}///') or { panic(err) }
assert x.body == 'Welcome to VWeb'
}

View file

@ -1,149 +0,0 @@
module main
import os
import x.vweb
import time
const known_users = ['bilbo', 'kent']
struct ServerContext {
vweb.Context
}
// Custom 404 page
pub fn (mut ctx ServerContext) not_found() vweb.Result {
ctx.res.set_status(.not_found)
return ctx.html('404 on "${ctx.req.url}"')
}
pub struct ServerApp {
port int
timeout int
global_config Config
}
struct Config {
max_ping int
}
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()
assert timeout > 0
spawn exit_after_timeout(timeout)
mut app := &ServerApp{
port: http_port
timeout: timeout
global_config: Config{
max_ping: 50
}
}
eprintln('>> webserver: pid: ${os.getpid()}, started on http://localhost:${app.port}/ , with maximum runtime of ${app.timeout} milliseconds.')
vweb.run_at[ServerApp, ServerContext](mut app, host: 'localhost', port: http_port, family: .ip)!
}
// pub fn (mut app ServerApp) init_server() {
//}
pub fn (mut app ServerApp) index(mut ctx ServerContext) vweb.Result {
assert app.global_config.max_ping == 50
return ctx.text('Welcome to VWeb')
}
pub fn (mut app ServerApp) simple(mut ctx ServerContext) vweb.Result {
return ctx.text('A simple result')
}
pub fn (mut app ServerApp) html_page(mut ctx ServerContext) vweb.Result {
return ctx.html('<h1>ok</h1>')
}
// the following serve custom routes
@['/:user/settings']
pub fn (mut app ServerApp) settings(mut ctx ServerContext, username string) vweb.Result {
if username !in known_users {
return ctx.not_found()
}
return ctx.html('username: ${username}')
}
@['/:user/:repo/settings']
pub fn (mut app ServerApp) user_repo_settings(mut ctx ServerContext, username string, repository string) vweb.Result {
if username !in known_users {
return ctx.not_found()
}
return ctx.html('username: ${username} | repository: ${repository}')
}
@['/json_echo'; post]
pub fn (mut app ServerApp) json_echo(mut ctx ServerContext) vweb.Result {
// eprintln('>>>>> received http request at /json_echo is: $app.req')
ctx.set_content_type(ctx.req.header.get(.content_type) or { '' })
return ctx.ok(ctx.req.data)
}
@['/login'; post]
pub fn (mut app ServerApp) login_form(mut ctx ServerContext, username string, password string) vweb.Result {
return ctx.html('username: x${username}x | password: x${password}x')
}
@['/form_echo'; post]
pub fn (mut app ServerApp) form_echo(mut ctx ServerContext) vweb.Result {
ctx.set_content_type(ctx.req.header.get(.content_type) or { '' })
return ctx.ok(ctx.form['foo'])
}
@['/file_echo'; post]
pub fn (mut app ServerApp) file_echo(mut ctx ServerContext) vweb.Result {
if 'file' !in ctx.files {
ctx.res.set_status(.internal_server_error)
return ctx.text('no file')
}
return ctx.text(ctx.files['file'][0].data)
}
@['/query_echo']
pub fn (mut app ServerApp) query_echo(mut ctx ServerContext, a string, b int) vweb.Result {
return ctx.text('a: x${a}x | b: x${b}x')
}
// Make sure [post] works without the path
@[post]
pub fn (mut app ServerApp) json(mut ctx ServerContext) vweb.Result {
// eprintln('>>>>> received http request at /json is: $app.req')
ctx.set_content_type(ctx.req.header.get(.content_type) or { '' })
return ctx.ok(ctx.req.data)
}
@[host: 'example.com']
@['/with_host']
pub fn (mut app ServerApp) with_host(mut ctx ServerContext) vweb.Result {
return ctx.ok('')
}
pub fn (mut app ServerApp) shutdown(mut ctx ServerContext) vweb.Result {
session_key := ctx.get_cookie('skey') or { return ctx.not_found() }
if session_key != 'superman' {
return ctx.not_found()
}
spawn app.exit_gracefully()
return ctx.ok('good bye')
}
fn (mut app ServerApp) exit_gracefully() {
eprintln('>> webserver: exit_gracefully')
time.sleep(100 * time.millisecond)
exit(0)
}

File diff suppressed because it is too large Load diff

View file

@ -1,48 +0,0 @@
module vweb
import time
// Note: to use live reloading while developing, the suggested workflow is doing:
// `v -d vweb_livereload watch --keep run your_vweb_server_project.v`
// in one shell, then open the start page of your vweb app in a browser.
//
// While developing, just open your files and edit them, then just save your
// changes. Once you save, the watch command from above, will restart your server,
// and your HTML pages will detect that shortly, then they will refresh themselves
// automatically.
// vweb_livereload_server_start records, when the vweb server process started.
// That is later used by the /script.js and /current endpoints, which are active,
// if you have compiled your vweb project with `-d vweb_livereload`, to detect
// whether the web server has been restarted.
const vweb_livereload_server_start = time.ticks().str()
// handle_vweb_livereload_current serves a small text file, containing the
// timestamp/ticks corresponding to when the vweb server process was started
@[if vweb_livereload ?]
fn (mut ctx Context) handle_vweb_livereload_current() {
ctx.send_response_to_client('text/plain', vweb_livereload_server_start)
}
// handle_vweb_livereload_script serves a small dynamically generated .js file,
// that contains code for polling the vweb server, and reloading the page, if it
// detects that the vweb server is newer than the vweb server, that served the
// .js file originally.
@[if vweb_livereload ?]
fn (mut ctx Context) handle_vweb_livereload_script() {
res := '"use strict";
function vweb_livereload_checker_fn(started_at) {
fetch("/vweb_livereload/" + started_at + "/current", { cache: "no-cache" })
.then(response=>response.text())
.then(function(current_at) {
// console.log(started_at); console.log(current_at);
if(started_at !== current_at){
// the app was restarted on the server:
window.location.reload();
}
});
}
const vweb_livereload_checker = setInterval(vweb_livereload_checker_fn, ${ctx.livereload_poll_interval_ms}, "${vweb_livereload_server_start}");
'
ctx.send_response_to_client('text/javascript', res)
}