blob: 4dfb9d56b6e2007d515c76741fd7a3f0dad7181d [file] [log] [blame]
/*****************************************************************************\
* operations.c - Slurm REST API http operations handlers
*****************************************************************************
* Copyright (C) SchedMD LLC.
*
* This file is part of Slurm, a resource management program.
* For details, see <https://slurm.schedmd.com/>.
* Please also read the included file: DISCLAIMER.
*
* Slurm is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* In addition, as a special exception, the copyright holders give permission
* to link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two. You must obey the GNU
* General Public License in all respects for all of the code used other than
* OpenSSL. If you modify file(s) with this exception, you may extend this
* exception to your version of the file(s), but you are not obligated to do
* so. If you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files in
* the program, then also delete it here.
*
* Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with Slurm; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\*****************************************************************************/
#include "config.h"
#include <unistd.h>
#include "slurm/slurm.h"
#include "src/common/list.h"
#include "src/common/log.h"
#include "src/common/xassert.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"
#include "src/interfaces/serializer.h"
#include "src/slurmrestd/operations.h"
#include "src/slurmrestd/rest_auth.h"
static pthread_rwlock_t paths_lock = PTHREAD_RWLOCK_INITIALIZER;
static list_t *paths = NULL;
static data_parser_t **parsers; /* symlink to parser array */
#define MAGIC_HEADER_ACCEPT 0xDF9EAABE
typedef struct {
#define PATH_MAGIC 0xDFFEA1AE
int magic; /* PATH_MAGIC */
/* unique tag per path */
int tag;
/* handler's ctxt callback to call on match */
const openapi_path_binding_t *op_path;
/* meta info from plugin */
const openapi_resp_meta_t *meta;
/* tag to hand to handler */
int callback_tag;
/* assigned parser */
data_parser_t *parser;
} path_t;
typedef struct {
int magic; /* MAGIC_HEADER_ACCEPT */
char *type; /* mime type and sub type unchanged */
float q; /* quality factor (priority) */
} http_header_accept_t;
static const char *_name(const on_http_request_args_t *args)
{
return conmgr_fd_get_name(args->context->con);
}
static void _check_path_magic(const path_t *path)
{
xassert(path->magic == PATH_MAGIC);
xassert(path->tag >= 0);
xassert(path->op_path->callback);
}
static void _free_path(void *x)
{
path_t *path = (path_t *) x;
if (!path)
return;
_check_path_magic(path);
path->magic = ~PATH_MAGIC;
xfree(path);
}
extern int init_operations(data_parser_t **init_parsers)
{
slurm_rwlock_wrlock(&paths_lock);
if (paths)
fatal_abort("%s called twice", __func__);
paths = list_create(_free_path);
parsers = init_parsers;
slurm_rwlock_unlock(&paths_lock);
return SLURM_SUCCESS;
}
extern void destroy_operations(void)
{
slurm_rwlock_wrlock(&paths_lock);
FREE_NULL_LIST(paths);
parsers = NULL;
slurm_rwlock_unlock(&paths_lock);
}
static int _match_path_key(void *x, void *ptr)
{
path_t *path = (path_t *)x;
int tag = *(int *) ptr;
_check_path_magic(path);
if (path->tag == tag)
return 1;
else
return 0;
}
static int _add_binded_path(const char *path_str,
const openapi_path_binding_t *op_path,
const openapi_resp_meta_t *meta,
data_parser_t *parser)
{
int tag, rc;
path_t *path;
slurm_rwlock_wrlock(&paths_lock);
rc = register_path_binding(path_str, op_path, meta, parser, &tag);
slurm_rwlock_unlock(&paths_lock);
if (rc == ESLURM_NOT_SUPPORTED)
return SLURM_SUCCESS;
if (rc)
return rc;
/* path should never be a duplicate */
xassert(!list_find_first(paths, _match_path_key, &tag));
/* add new path */
debug4("%s: new bound path %s with path_tag %d",
__func__, (path_str ? path_str : op_path->path), tag);
print_path_tag_methods(tag);
path = xmalloc(sizeof(*path));
path->magic = PATH_MAGIC;
path->tag = tag;
path->parser = parser;
path->op_path = op_path;
path->meta = meta;
list_append(paths, path);
return SLURM_SUCCESS;
}
extern int bind_operation_path(const openapi_path_binding_t *op_path,
const openapi_resp_meta_t *meta)
{
int rc = SLURM_SUCCESS;
data_t *resp;
if (!(op_path->flags & OP_BIND_DATA_PARSER)) {
data_parser_t *default_parser = NULL;
if (!parsers[0])
fatal("No data_parsers plugins loaded. Refusing to load.");
for (int i = 0; parsers[i]; i++) {
if (!xstrcmp(data_parser_get_plugin(parsers[i]),
SLURM_DATA_PARSER_VERSION)) {
default_parser = parsers[i];
break;
}
}
if (!default_parser)
default_parser = parsers[0];
return _add_binded_path(NULL, op_path, meta, default_parser);
}
resp = data_new();
xassert(xstrstr(op_path->path, OPENAPI_DATA_PARSER_PARAM));
for (int i = 0; !rc && parsers[i]; i++) {
char *path = xstrdup(op_path->path);
xstrsubstitute(path, OPENAPI_DATA_PARSER_PARAM,
data_parser_get_plugin_version(parsers[i]));
rc = _add_binded_path(path, op_path, meta, parsers[i]);
xfree(path);
if (rc)
break;
}
FREE_NULL_DATA(resp);
return rc;
}
static int _operations_router_reject(const on_http_request_args_t *args,
const char *err,
http_status_code_t err_code,
const char *body_encoding)
{
send_http_response_args_t send_args = {
.con = args->context->con,
.headers = list_create(NULL),
.http_major = args->http_major,
.http_minor = args->http_minor,
.status_code = err_code,
.body = err,
.body_encoding = (body_encoding ? body_encoding : "text/plain"),
.body_length = (err ? strlen(err) : 0),
};
http_header_entry_t close = {
.name = "Connection",
.value = "Close",
};
/* Always warn that connection will be closed after the body is sent */
list_append(send_args.headers, &close);
(void) send_http_response(&send_args);
/* close connection on error */
conmgr_queue_close_fd(args->context->con);
FREE_NULL_LIST(send_args.headers);
return SLURM_ERROR;
}
static int _resolve_path(on_http_request_args_t *args, int *path_tag,
data_t *params)
{
data_t *path = parse_url_path(args->path, true, false);
if (!path)
return _operations_router_reject(
args, "Unable to parse URL path.",
HTTP_STATUS_CODE_ERROR_BAD_REQUEST, NULL);
/* attempt to identify path leaf types */
(void) data_convert_tree(path, DATA_TYPE_NONE);
*path_tag = find_path_tag(path, params, args->method);
FREE_NULL_DATA(path);
if (*path_tag == -1)
return _operations_router_reject(
args,
"Unable find requested URL. Please view /openapi/v3 for API reference.",
HTTP_STATUS_CODE_ERROR_NOT_FOUND, NULL);
else if (*path_tag == -2)
return _operations_router_reject(
args,
"Requested REST method is not defined at URL. Please view /openapi/v3 for API reference.",
HTTP_STATUS_CODE_ERROR_METHOD_NOT_ALLOWED, NULL);
else
return SLURM_SUCCESS;
}
static int _get_query(on_http_request_args_t *args, data_t **query,
const char *read_mime)
{
int rc = SLURM_SUCCESS;
/*
* RFC 7230 3.3:
* The presence of a message body in a request is signaled by a
* Content-Length or Transfer-Encoding header field.
*/
if (args->body_length > 0)
rc = serialize_g_string_to_data(query, args->body,
args->body_length, read_mime);
else
rc = serialize_g_string_to_data(
query, args->query,
(args->query ? strlen(args->query) : 0), read_mime);
if (rc || !*query)
return _operations_router_reject(
args, "Unable to parse query.",
HTTP_STATUS_CODE_ERROR_BAD_REQUEST, NULL);
else
return SLURM_SUCCESS;
}
static void _parse_http_accept_entry(char *entry, list_t *l)
{
char *save_ptr = NULL;
char *token = NULL;
char *buffer = xstrdup(entry);
http_header_accept_t *act = xmalloc(sizeof(*act));
act->magic = MAGIC_HEADER_ACCEPT;
act->type = NULL;
act->q = 1; /* default to 1 per rfc7231:5.3.1 */
token = strtok_r(buffer, ";", &save_ptr);
if (token) {
/* first token is the mime type */
xstrtrim(token);
act->type = xstrdup(token);
}
while ((token = strtok_r(NULL, ",", &save_ptr))) {
xstrtrim(token);
sscanf(token, "q=%f", &act->q);
}
xfree(buffer);
debug5("%s: found %s with q=%f", __func__, act->type, act->q);
list_append(l, act);
}
static int _compare_q(void *x, void *y)
{
http_header_accept_t **xobj_ptr = x;
http_header_accept_t **yobj_ptr = y;
http_header_accept_t *xobj = *xobj_ptr;
http_header_accept_t *yobj = *yobj_ptr;
xassert(xobj->magic == MAGIC_HEADER_ACCEPT);
xassert(yobj->magic == MAGIC_HEADER_ACCEPT);
if (xobj->q < yobj->q)
return -1;
else if (xobj->q > yobj->q)
return 1;
return 0;
}
static void _http_accept_list_delete(void *x)
{
http_header_accept_t *obj = (http_header_accept_t *) x;
if (!obj)
return;
xassert(obj->magic == MAGIC_HEADER_ACCEPT);
obj->magic = ~MAGIC_HEADER_ACCEPT;
xfree(obj->type);
xfree(obj);
}
static list_t *_parse_http_accept(const char *accept)
{
list_t *l = list_create(_http_accept_list_delete);
xassert(accept);
char *save_ptr = NULL;
char *token = NULL;
char *buffer = xstrdup(accept);
token = strtok_r(buffer, ",", &save_ptr);
while (token) {
xstrtrim(token);
_parse_http_accept_entry(token, l);
token = strtok_r(NULL, ",", &save_ptr);
}
xfree(buffer);
list_sort(l, _compare_q);
return l;
}
static int _resolve_mime(on_http_request_args_t *args, const char **read_mime,
const char **write_mime, const char **plugin_ptr)
{
*read_mime = args->content_type;
//TODO: check Content-encoding and make sure it is identity only!
//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (!*read_mime) {
*read_mime = MIME_TYPE_URL_ENCODED;
debug4("%s: [%s] did not provide a known content type header. Assuming URL encoded.",
__func__, _name(args));
}
if (args->accept) {
list_t *accept = _parse_http_accept(args->accept);
http_header_accept_t *ptr = NULL;
list_itr_t *itr = list_iterator_create(accept);
while ((ptr = list_next(itr))) {
xassert(ptr->magic == MAGIC_HEADER_ACCEPT);
debug4("%s: [%s] accepts %s with q=%f",
__func__, _name(args), ptr->type, ptr->q);
if ((*write_mime = resolve_mime_type(ptr->type,
plugin_ptr))) {
debug4("%s: [%s] found accepts %s=%s with q=%f",
__func__, _name(args), ptr->type,
*write_mime, ptr->q);
break;
} else {
debug4("%s: [%s] rejecting accepts %s with q=%f",
__func__, _name(args), ptr->type,
ptr->q);
}
}
list_iterator_destroy(itr);
FREE_NULL_LIST(accept);
} else {
debug3("%s: [%s] Accept header not specified. Defaulting to JSON.",
__func__, _name(args));
*write_mime = MIME_TYPE_JSON;
}
if (!*write_mime)
return _operations_router_reject(
args, "Accept content type is unknown",
HTTP_STATUS_CODE_ERROR_UNSUPPORTED_MEDIA_TYPE, NULL);
/*
* RFC7230 3.3: Allows for any request to have a BODY but doesn't require
* the server do anything with it.
* Request message framing is independent of method semantics, even
* if the method does not define any use for a message body.
* RFC7231 Appendix B:
* To be consistent with the method-neutral parsing algorithm of
* [RFC7230], the definition of GET has been relaxed so that
* requests can have a body, even though a body has no meaning for
* GET. (Section 4.3.1)
*
* In order to avoid confusing the client when their query or body gets
* ignored, reject request when both query and body are provided.
*/
if ((args->body_length > 0) && args->query && args->query[0])
return _operations_router_reject(
args,
"Unexpected HTTP body provided when URL Query provided",
HTTP_STATUS_CODE_ERROR_BAD_REQUEST, NULL);
if (xstrcasecmp(*read_mime, MIME_TYPE_URL_ENCODED) &&
(args->body_length == 0)) {
/*
* RFC7273#3.1.1.5 only specifies a sender SHOULD send
* the correct content-type header but allows for them to be
* wrong and expects the server to handle that gracefully.
*
* We will instead override the mime type if there is empty body
* content to avoid unneccesssily rejecting otherwise compliant
* requests.
*/
debug("%s: [%s] Overriding content type from %s to %s for %s",
__func__, _name(args), *read_mime, MIME_TYPE_URL_ENCODED,
get_http_method_string(args->method));
*read_mime = MIME_TYPE_URL_ENCODED;
}
debug3("%s: [%s] mime read: %s write: %s",
__func__, _name(args), *read_mime, *write_mime);
return SLURM_SUCCESS;
}
static int _call_handler(on_http_request_args_t *args, data_t *params,
data_t *query, const openapi_path_binding_t *op_path,
int callback_tag, const char *write_mime,
data_parser_t *parser, const openapi_resp_meta_t *meta,
const char *plugin)
{
int rc;
data_t *resp = data_new();
char *body = NULL;
http_status_code_t e;
xassert(op_path);
debug3("%s: [%s] BEGIN: calling ctxt handler: 0x%"PRIXPTR"[%d] for path: %s",
__func__, _name(args), (uintptr_t) op_path->callback,
callback_tag, args->path);
rc = wrap_openapi_ctxt_callback(_name(args), args->method, params,
query, callback_tag, resp,
args->context->auth, parser, op_path,
meta);
/*
* Clear auth context after callback is complete. Client has to provide
* full auth for every request already.
*/
FREE_NULL_REST_AUTH(args->context->auth);
if (data_get_type(resp) != DATA_TYPE_NULL) {
int rc2;
serializer_flags_t sflags = SER_FLAGS_NONE;
if (data_parser_g_is_complex(parser))
sflags |= SER_FLAGS_COMPLEX;
rc2 = serialize_g_data_to_string(&body, NULL, resp, write_mime,
sflags);
if (!rc)
rc = rc2;
}
if (rc == SLURM_NO_CHANGE_IN_DATA) {
/*
* RFC#7232 Section:4.1
*
* Send minimal response that nothing has changed
*
*/
send_http_response_args_t send_args = {
.con = args->context->con,
.http_major = args->http_major,
.http_minor = args->http_minor,
.status_code = HTTP_STATUS_CODE_REDIRECT_NOT_MODIFIED,
};
e = send_args.status_code;
rc = send_http_response(&send_args);
} else if (rc && (rc != ESLURM_REST_EMPTY_RESULT)) {
e = HTTP_STATUS_CODE_SRVERR_INTERNAL;
if (rc == ESLURM_REST_INVALID_QUERY)
e = HTTP_STATUS_CODE_ERROR_UNPROCESSABLE_CONTENT;
else if (rc == ESLURM_REST_FAIL_PARSING)
e = HTTP_STATUS_CODE_ERROR_BAD_REQUEST;
else if (rc == ESLURM_REST_INVALID_JOBS_DESC)
e = HTTP_STATUS_CODE_ERROR_BAD_REQUEST;
else if (rc == ESLURM_DATA_UNKNOWN_MIME_TYPE)
e = HTTP_STATUS_CODE_ERROR_UNSUPPORTED_MEDIA_TYPE;
else if (rc == ESLURM_INVALID_JOB_ID)
e = HTTP_STATUS_CODE_ERROR_NOT_FOUND;
else if ((rc == SLURM_PROTOCOL_SOCKET_ZERO_BYTES_SENT) ||
(rc == SLURM_COMMUNICATIONS_CONNECTION_ERROR) ||
(rc == SLURM_COMMUNICATIONS_SEND_ERROR) ||
(rc == SLURM_COMMUNICATIONS_RECEIVE_ERROR) ||
(rc == SLURM_COMMUNICATIONS_SHUTDOWN_ERROR) ||
(rc == SLURMCTLD_COMMUNICATIONS_CONNECTION_ERROR) ||
(rc == SLURMCTLD_COMMUNICATIONS_SEND_ERROR) ||
(rc == SLURMCTLD_COMMUNICATIONS_RECEIVE_ERROR) ||
(rc == SLURMCTLD_COMMUNICATIONS_SHUTDOWN_ERROR) ||
(rc == SLURMCTLD_COMMUNICATIONS_BACKOFF) ||
(rc == ESLURM_DB_CONNECTION) ||
(rc == ESLURM_PROTOCOL_INCOMPLETE_PACKET))
e = HTTP_STATUS_CODE_SRVERR_BAD_GATEWAY;
else if (rc == SLURM_PROTOCOL_SOCKET_IMPL_TIMEOUT)
e = HTTP_STATUS_CODE_SRVERR_GATEWAY_TIMEOUT;
else if (rc == SLURM_PROTOCOL_AUTHENTICATION_ERROR)
e = HTTP_STATUS_CODE_SRVERR_NETWORK_AUTH_REQ;
rc = _operations_router_reject(args, body, e, write_mime);
} else {
send_http_response_args_t send_args = {
.con = args->context->con,
.http_major = args->http_major,
.http_minor = args->http_minor,
.status_code = HTTP_STATUS_CODE_SUCCESS_OK,
.body = NULL,
.body_length = 0,
};
if (body) {
send_args.body = body;
send_args.body_length = strlen(body);
send_args.body_encoding = write_mime;
}
rc = send_http_response(&send_args);
e = send_args.status_code;
}
debug3("%s: [%s] END: calling handler: (0x%"PRIXPTR") callback_tag %d for path: %s rc[%d]=%s status[%d]=%s",
__func__, _name(args), (uintptr_t) op_path->callback,
callback_tag, args->path, rc, slurm_strerror(rc), e,
get_http_status_code_string(e));
xfree(body);
FREE_NULL_DATA(resp);
return rc;
}
extern int operations_router(on_http_request_args_t *args)
{
int rc = SLURM_SUCCESS;
data_t *query = NULL;
data_t *params = NULL;
int path_tag;
path_t *path = NULL;
int callback_tag;
const char *read_mime = NULL, *write_mime = NULL, *plugin = NULL;
data_parser_t *parser = NULL;
info("%s: [%s] %s %s",
__func__, _name(args), get_http_method_string(args->method),
args->path);
if ((rc = rest_authenticate_http_request(args))) {
error("%s: [%s] authentication failed: %s",
__func__, _name(args), slurm_strerror(rc));
_operations_router_reject(args, "Authentication failure",
HTTP_STATUS_CODE_ERROR_UNAUTHORIZED,
NULL);
return rc;
}
params = data_set_dict(data_new());
if ((rc = _resolve_path(args, &path_tag, params)))
goto cleanup;
/*
* Hold read lock while the callback is executing to avoid
* unbind of a function that is actively running
*/
slurm_rwlock_rdlock(&paths_lock);
if (!(path = list_find_first(paths, _match_path_key, &path_tag)))
fatal_abort("%s: found tag but missing path handler", __func__);
_check_path_magic(path);
/* clone over the callback info to release lock */
callback_tag = path->callback_tag;
parser = path->parser;
slurm_rwlock_unlock(&paths_lock);
debug5("%s: [%s] found callback handler: (0x%"PRIXPTR") callback_tag=%d path=%s parser=%s",
__func__, _name(args), (uintptr_t) path->op_path->callback,
callback_tag, args->path,
(parser ? data_parser_get_plugin(parser) : ""));
if ((rc = _resolve_mime(args, &read_mime, &write_mime, &plugin)))
goto cleanup;
if ((rc = _get_query(args, &query, read_mime)))
goto cleanup;
rc = _call_handler(args, params, query, path->op_path, callback_tag,
write_mime, parser, path->meta, plugin);
cleanup:
FREE_NULL_DATA(query);
FREE_NULL_DATA(params);
/* always clear the auth context */
FREE_NULL_REST_AUTH(args->context->auth);
return rc;
}