vlib: fix veb (#21345)

This commit is contained in:
Turiiya 2024-04-25 01:13:55 +02:00 committed by GitHub
parent 1aee2b567b
commit e24e905e51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1667 additions and 256 deletions

916
vlib/veb/README.md Normal file
View file

@ -0,0 +1,916 @@
# veb - 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 veb 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 veb 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.veb` 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 veb
pub struct User {
pub mut:
name string
id int
}
// Our context struct must embed `veb.Context`!
pub struct Context {
veb.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 veb. This is the index route
pub fn (app &App) index(mut ctx Context) veb.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
veb.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 `veb.Result`.
**Example:**
```v ignore
// This endpoint can be accessed via http://server:port/hello
pub fn (app &App) hello(mut ctx Context) veb.Result {
return ctx.text('Hello')
}
// This endpoint can be accessed via http://server:port/foo
@['/foo']
pub fn (app &App) world(mut ctx Context) veb.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) veb.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) veb.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) veb.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) veb.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) veb.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) veb.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=veb we
will see the text `Hello veb!`. 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) veb.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) veb.Result {
return app.text('Hello World')
}
@['/'; host: 'api.example.org']
pub fn (app &App) hello_api(mut ctx Context) veb.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) veb.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
veb 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) veb.Result {
return ctx.text('from with_parameter, path: "${path}"')
}
@['/normal']
pub fn (app &App) normal(mut ctx Context) veb.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() veb.Result {
// set HTTP status 404
ctx.res.set_status(.not_found)
return ctx.html('<h1>Page not found!</h1>')
}
```
## Static files and website
veb 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
`veb.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:**
> veb will recursively search the folder you mount; all the files inside that folder
> will be publicly available.
_main.v_
```v
module main
import veb
pub struct Context {
veb.Context
}
pub struct App {
veb.StaticHandler
}
fn main() {
mut app := &App{}
app.handle_static('static', false)!
veb.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, veb 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/veb/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, veb 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 veb, veb 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 veb 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 veb's middleware we have to embed `veb.Middleware` on our app struct and provide
the type of which context struct should be used.
**Example:**
```v ignore
pub struct App {
veb.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 {
veb.Context
pub mut:
has_accepted_cookies bool
}
```
In veb 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 veb whether it can continue or has to stop. If we send a
response to the client in a middleware function veb 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) veb.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
veb.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
veb 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`, veb 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`) veb 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 `veb.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 veb
pub struct Context {
veb.Context
}
pub struct App {
veb.Controller
}
// this endpoint will be available at '/'
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('from app')
}
pub struct Admin {}
// this endpoint will be available at '/admin/'
pub fn (app &Admin) index(mut ctx Context) veb.Result {
return ctx.text('from admin')
}
pub struct Foo {}
// this endpoint will be available at '/foo/'
pub fn (app &Foo) index(mut ctx Context) veb.Result {
return ctx.text('from foo')
}
fn main() {
mut app := &App{}
// register the controllers the same way as how we start a veb 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)!
veb.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) veb.Result {
return ctx.text('Admin')
}
```
When we registered the controller with
`app.register_controller[Admin, Context]('/admin', mut admin_app)!`
we told veb 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'`.
veb 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) veb.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) veb.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
veb has a number of utility methods that make it easier to handle requests and send responses.
These methods are available on `veb.Context` and directly on your own context struct if you
embed `veb.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) veb.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) veb.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) veb.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) veb.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) veb.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 veb:
- `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) veb.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) veb.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 veb 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 `veb.no_result()`. This function does nothing
and returns an empty `veb.Result` struct, letting veb 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 veb
pub struct Context {
veb.Context
}
pub struct App {}
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('hello!')
}
@['/long']
pub fn (app &App) long_response(mut ctx Context) veb.Result {
// let veb 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 veb is singlethreaded
spawn handle_connection(mut ctx.conn)
// we will send a custom response ourselves, so we can safely return an empty result
return veb.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{}
veb.run[App, Context](mut app, 8080)
}
```

177
vlib/veb/assets/README.md Normal file
View file

@ -0,0 +1,177 @@
# Assets
The asset manager for veb. 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 veb
import veb.assets
pub struct Context {
veb.Context
}
pub struct App {
pub mut:
am assets.AssetManager
}
fn main() {
mut app := &App{}
veb.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 veb's `StaticHandler` to serve
assets in a folder as static assets.
**Example:**
```v ignore
pub struct App {
veb.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
veb.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 {
veb.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
veb.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

@ -4,7 +4,7 @@ import crypto.md5
import os
import strings
import time
import x.vweb
import veb
pub enum AssetType {
css
@ -183,7 +183,7 @@ fn (mut am AssetManager) get_cache_key(file_path string, last_modified i64) stri
// this function is called
pub fn (mut am AssetManager) cleanup_cache() ! {
if am.cache_dir == '' {
return error('[vweb.assets]: cache directory is not valid')
return error('[veb.assets]: cache directory is not valid')
}
cached_files := os.ls(am.cache_dir)!
@ -205,12 +205,12 @@ pub fn (am AssetManager) exists(asset_type AssetType, include_name string) bool
return assets.any(it.include_name == include_name)
}
// include css/js files in your vweb app from templates
// include css/js files in your veb app from templates
// Example:
// ```html
// @{app.am.include(.css, 'main.css')}
// ```
pub fn (am AssetManager) include(asset_type AssetType, include_name string) vweb.RawHtml {
pub fn (am AssetManager) include(asset_type AssetType, include_name string) veb.RawHtml {
assets := am.get_assets(asset_type)
for asset in assets {
if asset.include_name == include_name {
@ -229,13 +229,13 @@ pub fn (am AssetManager) include(asset_type AssetType, include_name string) vweb
'<script src="${real_path}"></script>'
}
else {
eprintln('[vweb.assets] can only include css or js assets')
eprintln('[veb.assets] can only include css or js assets')
''
}
}
}
}
eprintln('[vweb.assets] no asset with include name "${include_name}" exists!')
eprintln('[veb.assets] no asset with include name "${include_name}" exists!')
return ''
}
@ -281,7 +281,7 @@ pub fn minify_css(css string) string {
for line in lines {
trimmed := line.trim_space()
if trimmed.len > 0 {
if trimmed != '' {
sb.write_string(trimmed)
}
}
@ -301,7 +301,7 @@ pub fn minify_js(js string) string {
for line in lines {
trimmed := line.trim_space()
if trimmed.len > 0 {
if trimmed != '' {
sb.write_string(trimmed)
sb.write_u8(` `)
}

View file

@ -1,7 +1,7 @@
import x.vweb.assets
import veb.assets
import os
const base_cache_dir = os.join_path(os.vtmp_dir(), 'xvweb_assets_test_cache')
const base_cache_dir = os.join_path(os.vtmp_dir(), 'veb_assets_test_cache')
fn testsuite_begin() {
os.mkdir_all(base_cache_dir) or {}
@ -143,7 +143,7 @@ fn test_minify_cache_last_modified() {
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
// if the file path was not modified, veb.assets didn't overwrite the file
assert js_assets[0].file_path == old_cached_path
}

View file

@ -9,14 +9,13 @@ All DBs are supported.
## Usage
```v
import x.vweb
import veb
import db.pg
import veb.auth
pub struct App {
vweb.StaticHandler
veb.StaticHandler
pub mut:
db pg.DB
auth auth.Auth[pg.DB] // or auth.Auth[sqlite.DB] etc
@ -25,7 +24,7 @@ pub mut:
const port = 8081
pub struct Context {
vweb.Context
veb.Context
current_user User
}
@ -41,11 +40,11 @@ fn main() {
db: pg.connect(host: 'localhost', user: 'postgres', password: '', dbname: 'postgres')!
}
app.auth = auth.new(app.db)
vweb.run[App, Context](mut app, port)
veb.run[App, Context](mut app, port)
}
@[post]
pub fn (mut app App) register_user(mut ctx Context, name string, password string) vweb.Result {
pub fn (mut app App) register_user(mut ctx Context, name string, password string) veb.Result {
salt := auth.generate_salt()
new_user := User{
name: name
@ -68,7 +67,7 @@ pub fn (mut app App) register_user(mut ctx Context, name string, password string
}
@[post]
pub fn (mut app App) login_post(mut ctx Context, name string, password string) vweb.Result {
pub fn (mut app App) login_post(mut ctx Context, name string, password string) veb.Result {
user := app.find_user_by_name(name) or {
ctx.error('Bad credentials')
return ctx.redirect('/login')
@ -90,5 +89,3 @@ pub fn (mut app App) find_user_by_name(name string) ?User {
return User{}
}
```

View file

@ -23,7 +23,7 @@ pub enum RedirectType {
@[heap]
pub struct Context {
mut:
// vweb will try to infer the content type base on file extension,
// veb 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
@ -32,14 +32,14 @@ mut:
// 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
// how the http response should be handled by veb'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.
// time.ticks() from start of veb connection handle.
// You can use it to determine how much time is spent on your request.
page_gen_start i64
req http.Request
@ -84,27 +84,27 @@ pub fn (mut ctx Context) set_custom_header(key string, value string) ! {
// 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')
eprintln('[veb] 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 veb_livereload ? {
if mimetype == 'text/html' {
ctx.res.body = response.replace('</html>', '<script src="/vweb_livereload/${vweb_livereload_server_start}/script.js"></script>\n</html>')
ctx.res.body = response.replace('</html>', '<script src="/veb_livereload/${veb_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.len > 0 {
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')
// send veb's closing headers
ctx.res.header.set(.server, 'veb')
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
@ -119,7 +119,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, response strin
if ctx.takeover {
fast_send_resp(mut ctx.conn, ctx.res) or {}
}
// result is send in `vweb.v`, `handle_route`
// result is send in `veb.v`, `handle_route`
return Result{}
}
@ -148,7 +148,7 @@ pub fn (mut ctx Context) json_pretty[T](j T) Result {
// 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')
eprintln('[veb] file "${file_path}" does not exist')
return ctx.not_found()
}
@ -164,7 +164,7 @@ pub fn (mut ctx Context) file(file_path string) Result {
}
if content_type.len == 0 {
eprintln('[vweb] no MIME type found for extension "${ext}"')
eprintln('[veb] no MIME type found for extension "${ext}"')
return ctx.server_error('')
}
@ -173,18 +173,18 @@ pub fn (mut ctx Context) file(file_path string) Result {
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()}')
eprint('[veb] 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()}')
eprintln('[veb] 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()}')
eprintln('[veb] error while trying to read file: ${err.msg()}')
return ctx.server_error('could not read resource')
}
file.close()
@ -192,7 +192,7 @@ fn (mut ctx Context) send_file(content_type string, file_path string) Result {
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()}')
eprintln('[veb] 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)
@ -227,6 +227,7 @@ pub fn (mut ctx Context) server_error(msg string) Result {
@[params]
pub struct RedirectParams {
pub:
typ RedirectType
}
@ -263,7 +264,7 @@ pub fn (ctx &Context) get_cookie(key string) ?string {
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}')
eprintln('[veb] error setting cookie: name of cookie is invalid.\n${cookie}')
return
}
ctx.res.header.add(.set_cookie, cookie_raw)
@ -274,7 +275,7 @@ 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
// takeover_conn prevents veb 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.

View file

@ -48,7 +48,7 @@ pub fn controller[A, X](path string, mut global_app A) !&ControllerPath {
}
}
// create a new user context and pass the vweb's context
// create a new user context and pass the veb's context
mut user_context := X{}
user_context.Context = ctx

249
vlib/veb/csrf/README.md Normal file
View file

@ -0,0 +1,249 @@
# 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 veb 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 veb
import veb.csrf
pub struct Context {
veb.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 veb's middleware.
**Example:**
```v ignore
pub struct App {
veb.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))
veb.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) veb.Result {
// this function will set a cookie header and generate a CSRF token
ctx.set_csrf_token(mut ctx)
return $veb.html()
}
@[post]
fn (app &App) login(mut ctx, password string) veb.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) veb.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) veb.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

@ -7,7 +7,7 @@ import net.http
import net.urllib
import rand
import time
import x.vweb
import veb
@[params]
pub struct CsrfConfig {
@ -66,13 +66,13 @@ pub fn (ctx &CsrfContext) clear_csrf_token[T](mut user_context T) {
}
// csrf_token_input returns an HTML hidden input containing the csrf token
pub fn (ctx &CsrfContext) csrf_token_input() vweb.RawHtml {
pub fn (ctx &CsrfContext) csrf_token_input() veb.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]{
// middleware returns a handler that you can use with veb's middleware
pub fn middleware[T](config CsrfConfig) veb.MiddlewareOptions[T] {
return veb.MiddlewareOptions[T]{
after: false
handler: fn [config] [T](mut ctx T) bool {
ctx.config = config
@ -89,12 +89,12 @@ pub fn middleware[T](config CsrfConfig) vweb.MiddlewareOptions[T] {
// 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 {
pub fn set_token(mut ctx veb.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_time(), session_id, config.nonce_length)
cookie := generate_cookie(expire_time.unix_time(), token, config.secret)
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
@ -115,7 +115,7 @@ pub fn set_token(mut ctx vweb.Context, config &CsrfConfig) string {
// 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 {
pub fn protect(mut ctx veb.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
@ -145,7 +145,7 @@ pub fn protect(mut ctx vweb.Context, config &CsrfConfig) bool {
// 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_time()
now := time.now().unix()
expire_timestamp := data[0].i64()
if expire_timestamp < now {
// token has expired
@ -178,7 +178,7 @@ pub fn protect(mut ctx vweb.Context, config &CsrfConfig) bool {
}
// check_origin_and_referer validates the `Origin` and `Referer` headers.
fn check_origin_and_referer(ctx vweb.Context, config &CsrfConfig) bool {
fn check_origin_and_referer(ctx veb.Context, config &CsrfConfig) bool {
// wildcard allow all hosts NOT SAFE!
if '*' in config.allowed_hosts {
return true
@ -206,7 +206,7 @@ fn check_origin_and_referer(ctx vweb.Context, config &CsrfConfig) bool {
}
// request_is_invalid sends an http 403 response
fn request_is_invalid(mut ctx vweb.Context) {
fn request_is_invalid(mut ctx veb.Context) {
ctx.res.set_status(.forbidden)
ctx.text('Forbidden: Invalid or missing CSRF token')
}

View file

@ -2,8 +2,8 @@ import time
import net.http
import net.html
import os
import x.vweb
import x.vweb.csrf
import veb
import veb.csrf
const sport = 12385
const localserver = '127.0.0.1:${sport}'
@ -27,7 +27,7 @@ const csrf_config_origin = csrf.CsrfConfig{
// =====================================
fn test_set_token() {
mut ctx := vweb.Context{}
mut ctx := veb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
@ -37,7 +37,7 @@ fn test_set_token() {
}
fn test_protect() {
mut ctx := vweb.Context{}
mut ctx := veb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
@ -51,7 +51,7 @@ fn test_protect() {
cookie_map := {
csrf_config.cookie_name: cookie
}
ctx = vweb.Context{
ctx = veb.Context{
form: form
req: http.Request{
method: .post
@ -72,7 +72,7 @@ fn test_timeout() {
max_age: timeout
}
mut ctx := vweb.Context{}
mut ctx := veb.Context{}
token := csrf.set_token(mut ctx, short_time_config)
@ -88,7 +88,7 @@ fn test_timeout() {
cookie_map := {
short_time_config.cookie_name: cookie
}
ctx = vweb.Context{
ctx = veb.Context{
form: form
req: http.Request{
method: .post
@ -118,7 +118,7 @@ fn test_valid_origin() {
}
req.add_header(.origin, 'http://${allowed_origin}')
req.add_header(.referer, 'http://${allowed_origin}/test')
mut ctx := vweb.Context{
mut ctx := veb.Context{
form: form
req: req
}
@ -143,7 +143,7 @@ fn test_invalid_origin() {
cookies: cookie_map
}
req.add_header(.origin, 'http://${allowed_origin}')
mut ctx := vweb.Context{
mut ctx := veb.Context{
form: form
req: req
}
@ -156,7 +156,7 @@ fn test_invalid_origin() {
cookies: cookie_map
}
req.add_header(.referer, 'http://${allowed_origin}/test')
ctx = vweb.Context{
ctx = veb.Context{
form: form
req: req
}
@ -168,7 +168,7 @@ fn test_invalid_origin() {
method: .post
cookies: cookie_map
}
ctx = vweb.Context{
ctx = veb.Context{
form: form
req: req
}
@ -181,12 +181,12 @@ fn test_invalid_origin() {
// ================================
pub struct Context {
vweb.Context
veb.Context
csrf.CsrfContext
}
pub struct App {
vweb.Middleware[Context]
veb.Middleware[Context]
mut:
started chan bool
}
@ -195,7 +195,7 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
fn (app &App) index(mut ctx Context) vweb.Result {
fn (app &App) index(mut ctx Context) veb.Result {
ctx.set_csrf_token(mut ctx)
return ctx.html('<form action="/auth" method="post">
@ -206,14 +206,14 @@ fn (app &App) index(mut ctx Context) vweb.Result {
}
@[post]
fn (app &App) auth(mut ctx Context) vweb.Result {
fn (app &App) auth(mut ctx Context) veb.Result {
return ctx.ok('authenticated')
}
// App cleanup function
// ======================================
pub fn (mut app App) shutdown(mut ctx Context) vweb.Result {
pub fn (mut app App) shutdown(mut ctx Context) veb.Result {
spawn app.exit_gracefully()
return ctx.ok('good bye')
}
@ -241,7 +241,7 @@ fn test_run_app_in_background() {
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)
spawn veb.run_at[App, Context](mut app, port: sport, family: .ip)
_ := <-app.started
}
@ -328,7 +328,7 @@ fn testsuite_end() {
// Utility functions
fn get_token_cookie(session_id string) (string, string) {
mut ctx := vweb.Context{
mut ctx := veb.Context{
req: http.Request{
cookies: {
session_id_cookie_name: session_id

View file

@ -4,7 +4,7 @@ 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.
// for string interpolation inside veb templates.
// TODO: move it to template render
fn filter(s string) string {
return html.escape(s)

View file

@ -30,6 +30,7 @@ mut:
@[params]
pub struct MiddlewareOptions[T] {
pub:
handler fn (mut ctx T) bool @[required]
after bool
}
@ -250,7 +251,7 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
ctx.res.set_status(.forbidden)
ctx.text('invalid CORS origin')
$if vweb_trace_cors ? {
$if veb_trace_cors ? {
eprintln('[veb]: rejected CORS request from "${origin}". Reason: invalid origin')
}
return false
@ -264,7 +265,7 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
ctx.res.set_status(.method_not_allowed)
ctx.text('${ctx.req.method} requests are not allowed')
$if vweb_trace_cors ? {
$if veb_trace_cors ? {
eprintln('[veb]: rejected CORS request from "${origin}". Reason: invalid request method: ${ctx.req.method}')
}
return false
@ -277,7 +278,7 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
ctx.res.set_status(.forbidden)
ctx.text('invalid Header "${header}"')
$if vweb_trace_cors ? {
$if veb_trace_cors ? {
eprintln('[veb]: rejected CORS request from "${origin}". Reason: invalid header "${header}"')
}
return false
@ -285,7 +286,7 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
}
}
$if vweb_trace_cors ? {
$if veb_trace_cors ? {
eprintln('[veb]: received CORS request from "${origin}": HTTP ${ctx.req.method} ${ctx.req.url}')
}
@ -296,7 +297,7 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
// preflight request and validating the headers of a cross-origin request.
// Example:
// ```v
// app.use(vweb.cors[Context](vweb.CorsOptions{
// app.use(veb.cors[Context](veb.CorsOptions{
// origins: ['*']
// allowed_methods: [.get, .head, .patch, .put, .post, .delete]
// }))

65
vlib/veb/sse/README.md Normal file
View file

@ -0,0 +1,65 @@
# 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
veb isn't able to process any other requests.
We can let veb 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 `veb.no_result()`. veb will not close the connection and we can handle
the connection in a separate thread.
**Example:**
```v ignore
import veb.sse
// endpoint handler for SSE connections
fn (app &App) sse(mut ctx Context) veb.Result {
// let veb 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 veb.no_result()
}
fn handle_sse_conn(mut ctx Context) {
// pass veb.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,6 +1,6 @@
module sse
import x.vweb
import veb
import net
import strings
@ -36,7 +36,7 @@ pub mut:
}
// start an SSE connection
pub fn start_connection(mut ctx vweb.Context) &SSEConnection {
pub fn start_connection(mut ctx veb.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', '')

View file

@ -1,6 +1,6 @@
// vtest retry: 3
import x.vweb
import x.vweb.sse
import veb
import veb.sse
import time
import net.http
@ -9,7 +9,7 @@ const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
@ -21,10 +21,10 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
fn (app &App) sse(mut ctx Context) vweb.Result {
fn (app &App) sse(mut ctx Context) veb.Result {
ctx.takeover_conn()
spawn handle_sse_conn(mut ctx)
return vweb.no_result()
return veb.no_result()
}
fn handle_sse_conn(mut ctx Context) {
@ -45,7 +45,7 @@ fn testsuite_begin() {
exit(1)
}()
spawn vweb.run_at[App, Context](mut app, port: port, family: .ip)
spawn veb.run_at[App, Context](mut app, port: port, family: .ip)
// app startup time
_ := <-app.started
}

View file

@ -9,7 +9,7 @@ mut:
static_hosts map[string]string
}
// StaticHandler provides methods to handle static files in your vweb App
// StaticHandler provides methods to handle static files in your veb App
pub struct StaticHandler {
pub mut:
static_files map[string]string
@ -81,7 +81,7 @@ pub fn (mut sh StaticHandler) mount_static_folder_at(directory_path string, moun
// 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.len < 1 || mount_path[0] != `/` {
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()}')

View file

@ -1,4 +1,4 @@
import x.vweb
import veb
import time
import os
import net.http
@ -10,11 +10,11 @@ const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
vweb.Controller
veb.Controller
mut:
started chan bool
}
@ -23,32 +23,32 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('from app')
}
@['/conflict/test']
pub fn (app &App) conflicting(mut ctx Context) vweb.Result {
pub fn (app &App) conflicting(mut ctx Context) veb.Result {
return ctx.text('from conflicting')
}
pub struct Other {
vweb.Controller
veb.Controller
}
pub fn (app &Other) index(mut ctx Context) vweb.Result {
pub fn (app &Other) index(mut ctx Context) veb.Result {
return ctx.text('from other')
}
pub struct HiddenByOther {}
pub fn (app &HiddenByOther) index(mut ctx Context) vweb.Result {
pub fn (app &HiddenByOther) index(mut ctx Context) veb.Result {
return ctx.text('from hidden')
}
pub struct SubController {}
pub fn (app &SubController) index(mut ctx Context) vweb.Result {
pub fn (app &SubController) index(mut ctx Context) veb.Result {
return ctx.text('from sub')
}
@ -66,7 +66,7 @@ fn testsuite_begin() {
// 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)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
_ := <-app.started
spawn fn () {
@ -108,7 +108,7 @@ fn test_conflicting_controllers() {
assert true == false, 'this should not fail'
}
vweb.run_at[App, Context](mut app, port: port) or {
veb.run_at[App, Context](mut app, port: port) or {
assert err.msg() == 'conflicting paths: duplicate controller handling the route "/other"'
return
}
@ -123,7 +123,7 @@ fn test_conflicting_controller_routes() {
assert true == false, 'this should not fail'
}
vweb.run_at[App, Context](mut app, port: port) or {
veb.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
}

View file

@ -1,4 +1,4 @@
import x.vweb
import veb
import net.http
import os
import time
@ -7,17 +7,17 @@ const port = 13012
const localserver = 'http://localhost:${port}'
const exit_after = time.second * 10
const allowed_origin = 'https://vlang.io'
const cors_options = vweb.CorsOptions{
const cors_options = veb.CorsOptions{
origins: [allowed_origin]
allowed_methods: [.get, .head]
}
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
vweb.Middleware[Context]
veb.Middleware[Context]
mut:
started chan bool
}
@ -26,12 +26,12 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('index')
}
@[post]
pub fn (app &App) post(mut ctx Context) vweb.Result {
pub fn (app &App) post(mut ctx Context) veb.Result {
return ctx.text('post')
}
@ -44,9 +44,9 @@ fn testsuite_begin() {
}()
mut app := &App{}
app.use(vweb.cors[Context](cors_options))
app.use(veb.cors[Context](cors_options))
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
}

View file

@ -1,6 +1,6 @@
// vtest flaky: true
// vtest retry: 3
import x.vweb
import veb
import net.http
import time
import os
@ -11,7 +11,7 @@ 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')
const tmp_file = os.join_path(os.vtmp_dir(), 'veb_large_payload.txt')
pub struct App {
mut:
@ -22,21 +22,21 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
pub fn (mut app App) index(mut ctx Context) veb.Result {
return ctx.text('Hello V!')
}
@[post]
pub fn (mut app App) post_request(mut ctx Context) vweb.Result {
pub fn (mut app App) post_request(mut ctx Context) veb.Result {
return ctx.text(ctx.req.data)
}
pub fn (app &App) file(mut ctx Context) vweb.Result {
pub fn (app &App) file(mut ctx Context) veb.Result {
return ctx.file(tmp_file)
}
pub struct Context {
vweb.Context
veb.Context
}
fn testsuite_begin() {
@ -47,31 +47,31 @@ fn testsuite_begin() {
}()
mut app := &App{}
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
spawn veb.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
// veb reads a maximum of 4096KB per picoev loop cycle
// this test tests if veb 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`}
mut buf := []u8{len: veb.max_read * 10, init: `a`}
str := buf.bytestr()
mut x := http.post('${localserver}/post_request', str)!
assert x.body.len == vweb.max_read * 10
assert x.body.len == veb.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`}
mut buf := []u8{len: veb.max_read * 2, init: `a`}
str := buf.bytestr()
// make 1 header longer than vwebs max read limit
// make 1 header longer than vebs max read limit
mut x := http.fetch(http.FetchConfig{
url: localserver
header: http.new_custom_header_from_map({
@ -113,12 +113,12 @@ fn test_smaller_content_length() {
}
fn test_sendfile() {
mut buf := []u8{len: vweb.max_write * 10, init: `a`}
mut buf := []u8{len: veb.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
assert x.body.len == veb.max_write * 10
}
fn testsuite_end() {

View file

@ -1,4 +1,4 @@
import x.vweb
import veb
import net.http
import os
import time
@ -10,14 +10,14 @@ const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct Context {
vweb.Context
veb.Context
pub mut:
counter int
}
@[heap]
pub struct App {
vweb.Middleware[Context]
veb.Middleware[Context]
mut:
started chan bool
}
@ -26,25 +26,25 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('from index, ${ctx.counter}')
}
@['/bar/bar']
pub fn (app &App) bar(mut ctx Context) vweb.Result {
pub fn (app &App) bar(mut ctx Context) veb.Result {
return ctx.text('from bar, ${ctx.counter}')
}
pub fn (app &App) unreachable(mut ctx Context) vweb.Result {
pub fn (app &App) unreachable(mut ctx Context) veb.Result {
return ctx.text('should never be reachable!')
}
@['/nested/route/method']
pub fn (app &App) nested(mut ctx Context) vweb.Result {
pub fn (app &App) nested(mut ctx Context) veb.Result {
return ctx.text('from nested, ${ctx.counter}')
}
pub fn (app &App) after(mut ctx Context) vweb.Result {
pub fn (app &App) after(mut ctx Context) veb.Result {
return ctx.text('from after, ${ctx.counter}')
}
@ -87,7 +87,7 @@ fn testsuite_begin() {
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)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started

View file

@ -3,7 +3,7 @@ import net.http
import io
import os
import time
import x.vweb
import veb
const exit_after = time.second * 10
const port = 13009
@ -20,7 +20,7 @@ Accept: */*
const response_body = 'intact!'
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
@ -33,12 +33,12 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
pub fn (mut app App) index(mut ctx Context) veb.Result {
app.counter++
return ctx.text('${response_body}:${app.counter}')
}
pub fn (mut app App) reset(mut ctx Context) vweb.Result {
pub fn (mut app App) reset(mut ctx Context) veb.Result {
app.counter = 0
return ctx.ok('')
}
@ -47,7 +47,7 @@ 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)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 5)
_ := <-app.started
spawn fn () {

View file

@ -1,4 +1,4 @@
import x.vweb
import veb
import net.http
import os
import time
@ -10,7 +10,7 @@ const localserver = 'http://127.0.0.1:${port}'
const exit_after = time.second * 10
pub struct App {
vweb.StaticHandler
veb.StaticHandler
mut:
started chan bool
}
@ -19,17 +19,17 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
pub fn (mut app App) index(mut ctx Context) veb.Result {
return ctx.text('Hello V!')
}
@[post]
pub fn (mut app App) post_request(mut ctx Context) vweb.Result {
pub fn (mut app App) post_request(mut ctx Context) veb.Result {
return ctx.text(ctx.req.data)
}
pub struct Context {
vweb.Context
veb.Context
}
fn testsuite_begin() {
@ -51,7 +51,7 @@ fn run_app_test() {
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']
app.static_mime_types['.what'] = veb.mime_types['.txt']
if _ := app.handle_static('not_found', true) {
assert false, 'should throw directory not found error'
@ -75,7 +75,7 @@ fn run_app_test() {
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)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started
}
@ -109,7 +109,7 @@ 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.header.get(.content_type)! == veb.mime_types['.txt']
assert x.body.trim_space() == 'unknown_mime'
}

1
vlib/veb/tests/testdata/root.txt vendored Normal file
View file

@ -0,0 +1 @@
root

View file

@ -0,0 +1 @@
OK

View file

@ -0,0 +1 @@
sub

View file

@ -0,0 +1 @@
OK

View file

@ -0,0 +1 @@
sub

View file

@ -0,0 +1 @@
unknown_mime

View file

@ -26,15 +26,13 @@ struct Article {
text string
}
fn test_a_vweb_application_compiles() {
fn test_veb_application_compiles() {
spawn fn () {
time.sleep(15 * time.second)
// exit(0)
exit(0)
}()
mut app := &App{}
veb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 2) or {
panic(err)
}
spawn veb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
}

View file

@ -3,13 +3,13 @@
import os
import log
import time
import x.vweb
import veb
import net.http
const vexe = os.getenv('VEXE')
const vroot = os.dir(vexe)
const port = 48872
const welcome_text = 'Welcome to our simple vweb server'
const welcome_text = 'Welcome to our simple veb server'
// Use a known good http client like `curl` (if it exists):
const curl_executable = os.find_abs_path_of_executable('curl') or { '' }
@ -93,7 +93,7 @@ fn test_net_http_connecting_through_ipv6_works() {
//
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
@ -105,7 +105,7 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index(mut ctx Context) vweb.Result {
pub fn (mut app App) index(mut ctx Context) veb.Result {
return ctx.text(welcome_text)
}
@ -119,7 +119,7 @@ fn start_services() {
log.debug('starting webserver...')
mut app := &App{}
spawn vweb.run[App, Context](mut app, port)
spawn veb.run[App, Context](mut app, port)
_ := <-app.started
log.debug('webserver started')
}

View file

@ -10,13 +10,13 @@ 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 veb_logfile = os.getenv('VEB_LOGFILE')
const vroot = os.dir(vexe)
const serverexe = os.join_path(os.cache_dir(), 'vweb_test_server.exe')
const serverexe = os.join_path(os.cache_dir(), 'veb_test_server.exe')
const tcp_r_timeout = 10 * time.second
const tcp_w_timeout = 10 * time.second
// setup of vweb webserver
// setup of veb webserver
fn testsuite_begin() {
os.chdir(vroot) or {}
if os.exists(serverexe) {
@ -24,20 +24,20 @@ fn testsuite_begin() {
}
}
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')
fn test_simple_veb_app_can_be_compiled() {
// did_server_compile := os.system('${os.quoted_path(vexe)} -g -o ${os.quoted_path(serverexe)} vlib/veb/tests/veb_test_server.v')
did_server_compile := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(serverexe)} vlib/veb/tests/veb_test_server.v')
assert did_server_compile == 0
assert os.exists(serverexe)
}
fn test_a_simple_vweb_app_runs_in_the_background() {
fn test_a_simple_veb_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)} &'
if veb_logfile != '' {
suffix = ' 2>> ${os.quoted_path(veb_logfile)} >> ${os.quoted_path(veb_logfile)} &'
}
server_exec_cmd := '${os.quoted_path(serverexe)} ${sport} ${exit_after_time} ${suffix}'
$if debug_net_socket_client ? {
@ -58,21 +58,21 @@ fn test_a_simple_vweb_app_runs_in_the_background() {
// 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')
assert received.starts_with('HTTP/1.1 200 OK\r\n'), received
assert received.contains('Server: veb\r\n'), received
assert received.contains('Content-Length:'), received
assert received.contains('Connection: close\r\n'), received
}
fn test_a_simple_tcp_client_can_connect_to_the_vweb_server() {
fn test_a_simple_tcp_client_can_connect_to_the_veb_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')
assert received.contains('Content-Type: text/plain'), received
assert received.contains('Content-Length: 14'), received
assert received.ends_with('Welcome to veb'), received
}
fn test_a_simple_tcp_client_simple_route() {
@ -109,7 +109,7 @@ fn test_a_simple_tcp_client_html_page() {
// 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(.server)! == 'veb'
assert x.header.get(.content_length)!.int() > 0
}
@ -117,7 +117,7 @@ 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.body == 'Welcome to veb'
assert x.header.get(.connection)! == 'close'
}
@ -218,9 +218,9 @@ fn test_http_client_multipart_form_data() {
mut files := []http.FileData{}
files << http.FileData{
filename: 'vweb'
filename: 'veb'
content_type: 'text'
data: '"vweb test"'
data: '"veb test"'
}
mut form_config_files := http.PostMultipartFormConfig{
@ -351,11 +351,11 @@ ${config.content}'
// 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'
assert x.body == 'Welcome to veb'
x = http.get('http://${localserver}/') or { panic(err) }
assert x.body == 'Welcome to VWeb'
assert x.body == 'Welcome to veb'
x = http.get('http://${localserver}//') or { panic(err) }
assert x.body == 'Welcome to VWeb'
assert x.body == 'Welcome to veb'
x = http.get('http://${localserver}///') or { panic(err) }
assert x.body == 'Welcome to VWeb'
assert x.body == 'Welcome to veb'
}

View file

@ -1,17 +1,17 @@
module main
import os
import x.vweb
import veb
import time
const known_users = ['bilbo', 'kent']
struct ServerContext {
vweb.Context
veb.Context
}
// Custom 404 page
pub fn (mut ctx ServerContext) not_found() vweb.Result {
pub fn (mut ctx ServerContext) not_found() veb.Result {
ctx.res.set_status(.not_found)
return ctx.html('404 on "${ctx.req.url}"')
}
@ -34,7 +34,7 @@ fn exit_after_timeout(timeout_in_ms int) {
fn main() {
if os.args.len != 3 {
panic('Usage: `vweb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`')
panic('Usage: `veb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`')
}
http_port := os.args[1].int()
assert http_port > 0
@ -50,28 +50,28 @@ fn main() {
}
}
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)!
veb.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 {
pub fn (mut app ServerApp) index(mut ctx ServerContext) veb.Result {
assert app.global_config.max_ping == 50
return ctx.text('Welcome to VWeb')
return ctx.text('Welcome to veb')
}
pub fn (mut app ServerApp) simple(mut ctx ServerContext) vweb.Result {
pub fn (mut app ServerApp) simple(mut ctx ServerContext) veb.Result {
return ctx.text('A simple result')
}
pub fn (mut app ServerApp) html_page(mut ctx ServerContext) vweb.Result {
pub fn (mut app ServerApp) html_page(mut ctx ServerContext) veb.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 {
pub fn (mut app ServerApp) settings(mut ctx ServerContext, username string) veb.Result {
if username !in known_users {
return ctx.not_found()
}
@ -79,7 +79,7 @@ pub fn (mut app ServerApp) settings(mut ctx ServerContext, username string) vweb
}
@['/:user/:repo/settings']
pub fn (mut app ServerApp) user_repo_settings(mut ctx ServerContext, username string, repository string) vweb.Result {
pub fn (mut app ServerApp) user_repo_settings(mut ctx ServerContext, username string, repository string) veb.Result {
if username !in known_users {
return ctx.not_found()
}
@ -87,25 +87,25 @@ pub fn (mut app ServerApp) user_repo_settings(mut ctx ServerContext, username st
}
@['/json_echo'; post]
pub fn (mut app ServerApp) json_echo(mut ctx ServerContext) vweb.Result {
pub fn (mut app ServerApp) json_echo(mut ctx ServerContext) veb.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 {
pub fn (mut app ServerApp) login_form(mut ctx ServerContext, username string, password string) veb.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 {
pub fn (mut app ServerApp) form_echo(mut ctx ServerContext) veb.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 {
pub fn (mut app ServerApp) file_echo(mut ctx ServerContext) veb.Result {
if 'file' !in ctx.files {
ctx.res.set_status(.internal_server_error)
return ctx.text('no file')
@ -115,13 +115,13 @@ pub fn (mut app ServerApp) file_echo(mut ctx ServerContext) vweb.Result {
}
@['/query_echo']
pub fn (mut app ServerApp) query_echo(mut ctx ServerContext, a string, b int) vweb.Result {
pub fn (mut app ServerApp) query_echo(mut ctx ServerContext, a string, b int) veb.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 {
pub fn (mut app ServerApp) json(mut ctx ServerContext) veb.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)
@ -129,11 +129,11 @@ pub fn (mut app ServerApp) json(mut ctx ServerContext) vweb.Result {
@[host: 'example.com']
@['/with_host']
pub fn (mut app ServerApp) with_host(mut ctx ServerContext) vweb.Result {
pub fn (mut app ServerApp) with_host(mut ctx ServerContext) veb.Result {
return ctx.ok('')
}
pub fn (mut app ServerApp) shutdown(mut ctx ServerContext) vweb.Result {
pub fn (mut app ServerApp) shutdown(mut ctx ServerContext) veb.Result {
session_key := ctx.get_cookie('skey') or { return ctx.not_found() }
if session_key != 'superman' {
return ctx.not_found()

View file

@ -29,7 +29,7 @@ pub fn no_result() Result {
pub const methods_with_form = [http.Method.post, .put, .patch]
pub const headers_close = http.new_custom_header_from_map({
'Server': 'VWeb'
'Server': 'veb'
}) or { panic('should never fail') }
pub const http_302 = http.new_response(
@ -203,13 +203,14 @@ fn generate_routes[A, X](app &A) !map[string]Route {
return routes
}
// run - start a new VWeb server, listening to all available addresses, at the specified `port`
// run - start a new veb server, listening to all available addresses, at the specified `port`
pub fn run[A, X](mut global_app A, port int) {
run_at[A, X](mut global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
}
@[params]
pub struct RunParams {
pub:
// use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
family net.AddrFamily = .ip6
host string
@ -282,7 +283,7 @@ mut:
before_accept_loop()
}
// run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port`
// run_at - start a new veb server, listening only on a specific address `host`, at the specified `port`
// Example: veb.run_at(new_app(), veb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
@[direct_array_access; manualfree]
pub fn run_at[A, X](mut global_app A, params RunParams) ! {
@ -295,7 +296,7 @@ pub fn run_at[A, X](mut global_app A, params RunParams) ! {
if params.show_startup_message {
host := if params.host == '' { 'localhost' } else { params.host }
println('[Veb] Running app on http://${host}:${params.port}/')
println('[veb] Running app on http://${host}:${params.port}/')
}
flush_stdout()
@ -758,7 +759,7 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string
}
}
// send only the headers, because if the response body is too big, TcpConn code will
// actually block, because it has to wait for the socket to become ready to write. Vweb
// actually block, because it has to wait for the socket to become ready to write. veb
// will handle this case.
if !was_done && !user_context.Context.done && !user_context.Context.takeover {
eprintln('[veb] handler for route "${url.path}" does not send any data!')
@ -772,14 +773,14 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string
url_words := url.path.split('/').filter(it != '')
$if vweb_livereload ? {
if url.path.starts_with('/vweb_livereload/') {
$if veb_livereload ? {
if url.path.starts_with('/veb_livereload/') {
if url.path.ends_with('current') {
user_context.handle_vweb_livereload_current()
user_context.handle_veb_livereload_current()
return
}
if url.path.ends_with('script.js') {
user_context.handle_vweb_livereload_script()
user_context.handle_veb_livereload_script()
return
}
}
@ -897,7 +898,7 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string
method_args := params.clone()
if method_args.len + 1 != method.args.len {
eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})')
eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})')
}
app.$method(mut user_context, method_args)
return
@ -975,9 +976,9 @@ fn serve_if_static[A, X](app &A, mut user_context X, url urllib.URL, host string
}
static_file := app.static_files[asked_path] or { return false }
// StaticHandler ensures that the mime type exists on either the App or in vweb
// StaticHandler ensures that the mime type exists on either the App or in veb
ext := os.file_ext(static_file)
mut mime_type := app.static_mime_types[ext] or { vweb.mime_types[ext] }
mut mime_type := app.static_mime_types[ext] or { veb.mime_types[ext] }
static_host := app.static_hosts[asked_path] or { '' }
if static_file == '' || mime_type == '' {

48
vlib/veb/veb_livereload.v Normal file
View file

@ -0,0 +1,48 @@
module veb
import time
// Note: to use live reloading while developing, the suggested workflow is doing:
// `v -d veb_livereload watch --keep run your_veb_server_project.v`
// in one shell, then open the start page of your veb 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.
// veb_livereload_server_start records, when the veb server process started.
// That is later used by the /script.js and /current endpoints, which are active,
// if you have compiled your veb project with `-d veb_livereload`, to detect
// whether the web server has been restarted.
const veb_livereload_server_start = time.ticks().str()
// handle_veb_livereload_current serves a small text file, containing the
// timestamp/ticks corresponding to when the veb server process was started
@[if veb_livereload ?]
fn (mut ctx Context) handle_veb_livereload_current() {
ctx.send_response_to_client('text/plain', veb.veb_livereload_server_start)
}
// handle_veb_livereload_script serves a small dynamically generated .js file,
// that contains code for polling the veb server, and reloading the page, if it
// detects that the veb server is newer than the veb server, that served the
// .js file originally.
@[if veb_livereload ?]
fn (mut ctx Context) handle_veb_livereload_script() {
res := '"use strict";
function veb_livereload_checker_fn(started_at) {
fetch("/veb_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 veb_livereload_checker = setInterval(veb_livereload_checker_fn, ${ctx.livereload_poll_interval_ms}, "${veb.veb_livereload_server_start}");
'
ctx.send_response_to_client('text/javascript', res)
}

View file

@ -1,48 +0,0 @@
module veb
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', veb.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}, "${veb.vweb_livereload_server_start}");
'
ctx.send_response_to_client('text/javascript', res)
}