diff --git a/src/server/api_logs.v b/src/server/api_logs.v index 6728392..021c1ac 100644 --- a/src/server/api_logs.v +++ b/src/server/api_logs.v @@ -12,12 +12,8 @@ import models { BuildLog, BuildLogFilter } // v1_get_logs returns all build logs in the database. A 'target' query param can // optionally be added to limit the list of build logs to that repository. -['/api/v1/logs'; get] +['/api/v1/logs'; auth; get] fn (mut app App) v1_get_logs() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } @@ -27,24 +23,16 @@ fn (mut app App) v1_get_logs() web.Result { } // v1_get_single_log returns the build log with the given id. -['/api/v1/logs/:id'; get] +['/api/v1/logs/:id'; auth; get] fn (mut app App) v1_get_single_log(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - log := app.db.get_build_log(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(log)) } // v1_get_log_content returns the actual build log file for the given id. -['/api/v1/logs/:id/content'; get] +['/api/v1/logs/:id/content'; auth; get] fn (mut app App) v1_get_log_content(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - log := app.db.get_build_log(id) or { return app.not_found() } file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.target_id.str(), log.arch, @@ -63,12 +51,8 @@ fn parse_query_time(query string) ?time.Time { } // v1_post_log adds a new log to the database. -['/api/v1/logs'; post] +['/api/v1/logs'; auth; post] fn (mut app App) v1_post_log() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - // Parse query params start_time_int := app.query['startTime'].int() diff --git a/src/server/api_targets.v b/src/server/api_targets.v index 4cc3a58..c9e7963 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -7,12 +7,8 @@ import db import models { Target, TargetArch, TargetFilter } // v1_get_targets returns the current list of targets. -['/api/v1/targets'; get] +['/api/v1/targets'; auth; get] fn (mut app App) v1_get_targets() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } @@ -22,24 +18,16 @@ fn (mut app App) v1_get_targets() web.Result { } // v1_get_single_target returns the information for a single target. -['/api/v1/targets/:id'; get] +['/api/v1/targets/:id'; auth; get] fn (mut app App) v1_get_single_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - repo := app.db.get_target(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(repo)) } // v1_post_target creates a new target from the provided query string. -['/api/v1/targets'; post] +['/api/v1/targets'; auth; post] fn (mut app App) v1_post_target() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - mut params := app.query.clone() // If a repo is created without specifying the arch, we assume it's meant @@ -63,24 +51,16 @@ fn (mut app App) v1_post_target() web.Result { } // v1_delete_target removes a given target from the server's list. -['/api/v1/targets/:id'; delete] +['/api/v1/targets/:id'; auth; delete] fn (mut app App) v1_delete_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - app.db.delete_target(id) return app.json(http.Status.ok, new_response('Repo removed successfully.')) } // v1_patch_target updates a target's data with the given query params. -['/api/v1/targets/:id'; patch] +['/api/v1/targets/:id'; auth; patch] fn (mut app App) v1_patch_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - app.db.update_target(id, app.query) if 'arch' in app.query { diff --git a/src/server/auth.v b/src/server/auth.v deleted file mode 100644 index 7c8a676..0000000 --- a/src/server/auth.v +++ /dev/null @@ -1,12 +0,0 @@ -module server - -import net.http - -// is_authorized checks whether the provided API key is correct. -fn (mut app App) is_authorized() bool { - x_header := app.req.header.get_custom('X-Api-Key', http.HeaderQueryConfig{ exact: true }) or { - return false - } - - return x_header.trim_space() == app.conf.api_key -} diff --git a/src/server/repo.v b/src/server/repo.v index 242fd2d..5ed5d15 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -49,12 +49,8 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re } // put_package handles publishing a package to a repository. -['/:repo/publish'; post] +['/:repo/publish'; auth; post] fn (mut app App) put_package(repo string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - mut pkg_path := '' if length := app.req.header.get(.content_length) { diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v index 316b387..fdc40e8 100644 --- a/src/server/repo_remove.v +++ b/src/server/repo_remove.v @@ -5,12 +5,8 @@ import net.http import web.response { new_response } // delete_package tries to remove the given package. -['/:repo/:arch/:pkg'; delete] +['/:repo/:arch/:pkg'; auth; delete] fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { - if !app.is_authorized() { - return app.json(.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { app.lerror('Error while deleting package: $err.msg()') @@ -29,12 +25,8 @@ fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result } // delete_arch_repo tries to remove the given arch-repo. -['/:repo/:arch'; delete] +['/:repo/:arch'; auth; delete] fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_arch_repo(repo, arch) or { app.lerror('Error while deleting arch-repo: $err.msg()') @@ -53,12 +45,8 @@ fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { } // delete_repo tries to remove the given repo. -['/:repo'; delete] +['/:repo'; auth; delete] fn (mut app App) delete_repo(repo string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_repo(repo) or { app.lerror('Error while deleting repo: $err.msg()') diff --git a/src/server/server.v b/src/server/server.v index 1a9df3f..9903cea 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -73,6 +73,7 @@ pub fn server(conf Config) ? { web.run(&App{ logger: logger + api_key: conf.api_key conf: conf repo: repo db: db diff --git a/src/web/consts.v b/src/web/consts.v index 1b5bf08..df8cdb2 100644 --- a/src/web/consts.v +++ b/src/web/consts.v @@ -26,6 +26,14 @@ pub const ( value: 'text/plain' ).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( status: .not_found body: '404 Not Found' diff --git a/src/web/parse.v b/src/web/parse.v index a095f0c..ee7a72c 100644 --- a/src/web/parse.v +++ b/src/web/parse.v @@ -3,6 +3,10 @@ module web import net.urllib 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. fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { if attrs.len == 0 { @@ -32,7 +36,7 @@ fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { } i++ } - if x.len > 0 { + if x.len > 0 && x.any(!web.attrs_to_ignore.contains(it)) { return IError(http.UnexpectedExtraAttributeError{ attributes: x }) diff --git a/src/web/web.v b/src/web/web.v index 8434a80..1d1480f 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -18,6 +18,8 @@ pub struct Context { pub: // HTTP Request req http.Request + // API key used when authenticating requests + api_key string // TODO Response pub mut: // 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 & // sends it to the client. pub fn (mut ctx Context) send_response_header() ? { - mut resp := http.Response{ + mut resp := http.new_response( header: ctx.header.join(headers_close) - } + ) + resp.header.add(.content_type, ctx.content_type) resp.set_status(ctx.status) 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 } +// 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 HTTP_OK with json_s as payload with content-type `application/json` pub fn (mut ctx Context) json(status http.Status, j T) Result { ctx.status = status @@ -177,9 +189,12 @@ pub fn (mut ctx Context) file(f_path string) Result { 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) { mut parts := range_str.split_nth('=', 2) + // We only support the 'bytes' range type if parts[0] != 'bytes' { ctx.status = .requested_range_not_satisfiable ctx.header.delete(.content_length) @@ -376,6 +391,7 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { static_mime_types: app.static_mime_types reader: reader logger: app.logger + api_key: app.api_key } // Calling middleware... @@ -394,31 +410,27 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { // Used for route matching 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()` // should be called first. - if !route.path.contains('/:') && url_words == route_words { - // 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() + 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 } - return - } - if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { + // We found a match 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 + } - if params := route_matches(url_words, route_words) { method_args := params.clone() 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)')