feat(web): added authentication as function attribute

Jef Roosens 2022-09-04 19:32:22 +02:00
parent 9268ef0302
commit 4887af26d3
Signed by untrusted user: Jef Roosens
GPG Key ID: B75D4F293C7052DB
4 changed files with 47 additions and 21 deletions

View File

@ -73,6 +73,7 @@ pub fn server(conf Config) ? {
web.run(&App{ web.run(&App{
logger: logger logger: logger
api_key: conf.api_key
conf: conf conf: conf
repo: repo repo: repo
db: db db: db

View File

@ -26,6 +26,14 @@ pub const (
value: 'text/plain' value: 'text/plain'
).join(headers_close) ).join(headers_close)
) )
http_401 = http.new_response(
status: .unauthorized
body: '401 Unauthorized'
header: http.new_header(
key: .content_type
value: 'text/plain'
).join(headers_close)
)
http_404 = http.new_response( http_404 = http.new_response(
status: .not_found status: .not_found
body: '404 Not Found' body: '404 Not Found'

View File

@ -3,6 +3,10 @@ module web
import net.urllib import net.urllib
import net.http import net.http
// Method attributes that should be ignored when parsing, as they're used
// elsewhere.
const attrs_to_ignore = ['auth']
// Parsing function attributes for methods and path. // Parsing function attributes for methods and path.
fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { fn parse_attrs(name string, attrs []string) ?([]http.Method, string) {
if attrs.len == 0 { if attrs.len == 0 {
@ -32,7 +36,7 @@ fn parse_attrs(name string, attrs []string) ?([]http.Method, string) {
} }
i++ i++
} }
if x.len > 0 { if x.len > 0 && x.any(!web.attrs_to_ignore.contains(it)) {
return IError(http.UnexpectedExtraAttributeError{ return IError(http.UnexpectedExtraAttributeError{
attributes: x attributes: x
}) })

View File

@ -18,6 +18,8 @@ pub struct Context {
pub: pub:
// HTTP Request // HTTP Request
req http.Request req http.Request
// API key used when authenticating requests
api_key string
// TODO Response // TODO Response
pub mut: pub mut:
// TCP connection to client. // TCP connection to client.
@ -101,9 +103,10 @@ fn (mut ctx Context) send_custom_response(resp &http.Response) ? {
// send_response_header constructs a valid HTTP response with an empty body & // send_response_header constructs a valid HTTP response with an empty body &
// sends it to the client. // sends it to the client.
pub fn (mut ctx Context) send_response_header() ? { pub fn (mut ctx Context) send_response_header() ? {
mut resp := http.Response{ mut resp := http.new_response(
header: ctx.header.join(headers_close) header: ctx.header.join(headers_close)
} )
resp.header.add(.content_type, ctx.content_type)
resp.set_status(ctx.status) resp.set_status(ctx.status)
ctx.send_custom_response(resp)? ctx.send_custom_response(resp)?
@ -133,6 +136,15 @@ pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) bo
return true return true
} }
// is_authenticated checks whether the request passes a correct API key.
pub fn (ctx &Context) is_authenticated() bool {
if provided_key := ctx.req.header.get_custom('X-Api-Key') {
return provided_key == ctx.api_key
}
return false
}
// json<T> HTTP_OK with json_s as payload with content-type `application/json` // json<T> HTTP_OK with json_s as payload with content-type `application/json`
pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { pub fn (mut ctx Context) json<T>(status http.Status, j T) Result {
ctx.status = status ctx.status = status
@ -177,9 +189,12 @@ pub fn (mut ctx Context) file(f_path string) Result {
file.close() file.close()
} }
// Currently, this only supports a single provided range, e.g.
// bytes=0-1023, and not multiple ranges, e.g. bytes=0-50, 100-150
if range_str := ctx.req.header.get(.range) { if range_str := ctx.req.header.get(.range) {
mut parts := range_str.split_nth('=', 2) mut parts := range_str.split_nth('=', 2)
// We only support the 'bytes' range type
if parts[0] != 'bytes' { if parts[0] != 'bytes' {
ctx.status = .requested_range_not_satisfiable ctx.status = .requested_range_not_satisfiable
ctx.header.delete(.content_length) ctx.header.delete(.content_length)
@ -376,8 +391,10 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) {
static_mime_types: app.static_mime_types static_mime_types: app.static_mime_types
reader: reader reader: reader
logger: app.logger logger: app.logger
api_key: app.api_key
} }
// Calling middleware... // Calling middleware...
app.before_request() app.before_request()
@ -394,31 +411,27 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) {
// Used for route matching // Used for route matching
route_words := route.path.split('/').filter(it != '') route_words := route.path.split('/').filter(it != '')
// Route immediate matches first // Route immediate matches & index files first
// For example URL `/register` matches route `/:user`, but `fn register()` // For example URL `/register` matches route `/:user`, but `fn register()`
// should be called first. // should be called first.
if !route.path.contains('/:') && url_words == route_words { if (!route.path.contains('/:') && url_words == route_words)
|| (url_words.len == 0 && route_words == ['index'] && method.name == 'index') {
// Check whether the request is authorised
if 'auth' in method.attrs && !app.is_authenticated() {
conn.write(http_401.bytes()) or {}
return
}
// We found a match // We found a match
if head.method == .post && method.args.len > 0 {
// TODO implement POST requests
// Populate method args with form values
// mut args := []string{cap: method.args.len}
// for param in method.args {
// args << form[param.name]
// }
// app.$method(args)
} else {
app.$method() app.$method()
} return
} else if params := route_matches(url_words, route_words) {
// Check whether the request is authorised
if 'auth' in method.attrs && !app.is_authenticated() {
conn.write(http_401.bytes()) or {}
return return
} }
if url_words.len == 0 && route_words == ['index'] && method.name == 'index' {
app.$method()
return
}
if params := route_matches(url_words, route_words) {
method_args := params.clone() method_args := params.clone()
if method_args.len != method.args.len { if method_args.len != method.args.len {
eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)') eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)')