mirror of
https://github.com/vlang/v.git
synced 2025-09-16 16:02:29 +03:00
vlib: fix veb (#21345)
This commit is contained in:
parent
1aee2b567b
commit
e24e905e51
35 changed files with 1667 additions and 256 deletions
916
vlib/veb/README.md
Normal file
916
vlib/veb/README.md
Normal 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
177
vlib/veb/assets/README.md
Normal 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')}`
|
|
@ -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(` `)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
249
vlib/veb/csrf/README.md
Normal 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
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
65
vlib/veb/sse/README.md
Normal 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
|
|
@ -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', '')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()}')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
1
vlib/veb/tests/testdata/root.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
root
|
1
vlib/veb/tests/testdata/sub.folder/sub_folder/index.htm
vendored
Normal file
1
vlib/veb/tests/testdata/sub.folder/sub_folder/index.htm
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
OK
|
1
vlib/veb/tests/testdata/sub.folder/sub_folder/sub.txt
vendored
Normal file
1
vlib/veb/tests/testdata/sub.folder/sub_folder/sub.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
sub
|
1
vlib/veb/tests/testdata/sub_folder/index.htm
vendored
Normal file
1
vlib/veb/tests/testdata/sub_folder/index.htm
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
OK
|
1
vlib/veb/tests/testdata/sub_folder/sub.txt
vendored
Normal file
1
vlib/veb/tests/testdata/sub_folder/sub.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
sub
|
1
vlib/veb/tests/testdata/unknown_mime.what
vendored
Normal file
1
vlib/veb/tests/testdata/unknown_mime.what
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
unknown_mime
|
|
@ -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
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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'
|
||||
}
|
|
@ -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()
|
|
@ -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
48
vlib/veb/veb_livereload.v
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue