/*****************************************************************************\
 *  rest.c - Library for managing HPE Slingshot networks
 *****************************************************************************
 *  Copyright 2022-2023 Hewlett Packard Enterprise Development LP
 *  Written by Jim Nordby <james.nordby@hpe.com>
 *
 *  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"

#define _GNU_SOURCE

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define CURL_TRACE 0	/* Turn on curl debug tracing */
#if CURL_TRACE
#include <ctype.h>
#endif

#include "src/common/slurm_xlator.h"
#include "src/curl/slurm_curl.h"

#include "switch_hpe_slingshot.h"
#include "rest.h"

static char *_read_authfile(const char *auth_dir, const char *base);
static void _clear_auth_header(slingshot_rest_conn_t *conn);
static bool _get_auth_header(slingshot_rest_conn_t *conn,
			     struct curl_slist **headers, bool cache_use);
/*
 * If an error response was received, log it
 */
static void _log_rest_detail(const char *name, const char *method,
			     const char *url, json_object *respjson,
			     long status)
{
	json_object *detail = NULL;

	if (!json_object_object_get_ex(respjson, "detail", &detail)) {
		error("%s %s %s status %ld no error details",
			name, method, url, status);
	} else {
		error("%s %s %s status %ld: %s", name, method, url, status,
			json_object_get_string(detail));
	}
}

/*
 * Internals of REST POST/PATCH/GET/DELETE calls, with retries, etc.
 */
static json_object *_rest_call(slingshot_rest_conn_t *conn,
			       http_request_method_t request_method,
			       const char *urlsuffix, json_object *reqjson,
			       long *status, bool not_found_ok)
{
	json_object *respjson = NULL;
	struct curl_slist *headers = NULL;
	const char *req = NULL;
	bool use_cache = true;
	char *response_str = NULL, *username = NULL, *password = NULL;
	char *url = NULL;

	xassert(conn != NULL);
	xassert(urlsuffix != NULL);

	/* Create full URL */
	if (conn->mtls.enabled)
		url = xstrdup_printf("%s%s", conn->mtls.url, urlsuffix);
	else
		url = xstrdup_printf("%s%s", conn->base_url, urlsuffix);

	/* If present, dump JSON payload to string */
	if (reqjson) {
		if (!(req = json_object_to_json_string(reqjson))) {
			error("Couldn't dump JSON request: %m");
			goto err;
		}
	}

again:
	debug("%s %s url=%s data='%s'", conn->name,
	      get_http_method_string(request_method), url, req);

	/* Create header list */
	headers = curl_slist_append(headers, "Content-Type: application/json");
	if (!headers) {
		error("curl_slist_append failed to append Content-Type: %m");
		goto err;
	}
	if (!_get_auth_header(conn, &headers, use_cache))
		goto err;

	/* If using basic auth, add the user name and password */
	if ((conn->auth.auth_type == SLINGSHOT_AUTH_BASIC) &&
	    conn->auth.u.basic.user_name && conn->auth.u.basic.password) {
		username = conn->auth.u.basic.user_name;
		password = conn->auth.u.basic.password;
	}

	if (slurm_curl_request(req, url, username, password, conn->mtls.ca_path,
			       conn->mtls.cert_path, conn->mtls.key_path,
			       headers, conn->timeout, &response_str, status,
			       request_method, false, conn->mtls.enabled))
		goto err;

	if (((*status == HTTP_UNAUTHORIZED) || (*status == HTTP_FORBIDDEN)) &&
	    (conn->auth.auth_type == SLINGSHOT_AUTH_OAUTH) && use_cache) {
		debug("%s %s %s unauthorized status %ld, retrying", conn->name,
		      get_http_method_string(request_method), url, *status);
		/*
		 * On HTTP_UNAUTHORIZED, free auth header and re-cache token
		 */
		curl_slist_free_all(headers);
		headers = NULL;
		use_cache = false;
		xfree(response_str);
		goto again;
	}

	/* Decode response into JSON */
	if (response_str && response_str[0]) {
		enum json_tokener_error jerr;
		respjson = json_tokener_parse_verbose(response_str, &jerr);
		if (respjson == NULL) {
			error("Couldn't decode %s response: %s (data '%s')",
			      conn->name, json_tokener_error_desc(jerr),
			      response_str);
			goto err;
		}
	} else if (request_method != HTTP_REQUEST_DELETE) {
		debug("%s %s %s No response data received %ld",
		      conn->name, get_http_method_string(request_method), url,
		      *status);
		goto err;
	}

	if (((*status >= HTTP_OK) && (*status <= HTTP_LAST_OK)) ||
		(*status == HTTP_NOT_FOUND && not_found_ok)) {
		debug("%s %s %s successful (%ld)", conn->name,
		      get_http_method_string(request_method), url, *status);
	} else {
		_log_rest_detail(conn->name,
				 get_http_method_string(request_method), url,
				 respjson, *status);
		if (json_object_put(respjson))
			respjson = NULL;
		goto err;
	}

err:
	curl_slist_free_all(headers);
	xfree(url);
	xfree(response_str);
	return respjson; /* NULL on error */
}


/*
 * POST with JSON payload, and return the response (or NULL on error)
 */
extern json_object *slingshot_rest_post(slingshot_rest_conn_t *conn,
					const char *urlsuffix,
					json_object *reqjson, long *status)
{
	return _rest_call(conn, HTTP_REQUEST_POST, urlsuffix, reqjson, status,
			  false);
}

/*
 * PATCH with JSON payload, and return the response (or NULL on error)
 */
extern json_object *slingshot_rest_patch(slingshot_rest_conn_t *conn,
					 const char *urlsuffix,
					 json_object *reqjson, long *status)
{
	return _rest_call(conn, HTTP_REQUEST_PATCH, urlsuffix, reqjson, status,
			  true);
}

/*
 * Do a GET from the requested URL; return the JSON response,
 * or NULL on error
 */
extern json_object *slingshot_rest_get(slingshot_rest_conn_t *conn,
				       const char *urlsuffix, long *status)
{
	return _rest_call(conn, HTTP_REQUEST_GET, urlsuffix, NULL, status,
			  true);
}

/*
 * DELETE the given URL; return true on success
 */
extern bool slingshot_rest_delete(slingshot_rest_conn_t *conn,
				  const char *urlsuffix, long *status)
{
	/* Only delete if we successfully POSTed before */
	if (!conn || !conn->base_url)
		return false;

	/* Don't need the response. Just free it with json_object_put */
	json_object_put(_rest_call(conn, HTTP_REQUEST_DELETE, urlsuffix, NULL,
				   status, false));

	if ((*status >= HTTP_OK) && (*status <= HTTP_LAST_OK))
		return true;
	else
		return false;
}

/*
 * Generic handle set up function for network connections to use
 */
extern bool slingshot_rest_connection(slingshot_rest_conn_t *conn,
				      const char *url,
				      slingshot_rest_auth_t auth_type,
				      const char *auth_dir,
				      const char *basic_user,
				      const char *basic_pwdfile,
				      bool mtls_enabled,
				      const char *mtls_ca_path,
				      const char *mtls_cert_path,
				      const char *mtls_key_path,
				      const char *mtls_url, int timeout,
				      int connect_timeout,
				      const char *conn_name)
{
	memset(conn, 0, sizeof(*conn));
	switch (auth_type) {
	case SLINGSHOT_AUTH_BASIC:
		conn->auth.auth_type = auth_type;
		conn->auth.u.basic.user_name = xstrdup(basic_user);
		if (!(conn->auth.u.basic.password = _read_authfile(
						auth_dir, basic_pwdfile)))
			return false;
		break;
	case SLINGSHOT_AUTH_OAUTH:
	case SLINGSHOT_AUTH_NONE:
		conn->auth.auth_type = auth_type;
		break;
	default:
		error("Invalid auth_type value %u", auth_type);
		return false;
	}
	conn->name = xstrdup(conn_name);
	conn->base_url = xstrdup(url);
	conn->auth.auth_dir = xstrdup(auth_dir);
	conn->timeout = timeout;
	conn->connect_timeout = connect_timeout;

	conn->mtls.enabled = mtls_enabled;
	if (mtls_enabled) {
		conn->mtls.ca_path = xstrdup(mtls_ca_path);
		conn->mtls.cert_path = xstrdup(mtls_cert_path);
		conn->mtls.key_path = xstrdup(mtls_key_path);
		conn->mtls.url = xstrdup(mtls_url);
	}

	/*
	 * Attempt to get an OAUTH token for later use
	 * (returns immediately if not OAUTH)
	 */
	if (!_get_auth_header(conn, NULL, false))
		return false;

	return true;
}

/*
 * Free data (including auth data) in this connection
 */
extern void slingshot_rest_destroy_connection(slingshot_rest_conn_t *conn)
{
	xfree(conn->name);
	xfree(conn->base_url);
	if (conn->auth.auth_type == SLINGSHOT_AUTH_BASIC) {
		xfree(conn->auth.u.basic.user_name);
		if (conn->auth.u.basic.password) {
			memset((void *) conn->auth.u.basic.password, 0,
			       strlen(conn->auth.u.basic.password));
			xfree(conn->auth.u.basic.password);
		}
	}
	xfree(conn->auth.auth_dir);
	xfree(conn->mtls.ca_path);
	xfree(conn->mtls.cert_path);
	xfree(conn->mtls.key_path);
	xfree(conn->mtls.url);
	_clear_auth_header(conn);
}

/*
 * Return buffer with contents of authentication file with
 * pathname <auth_dir>/<base>; strip any trailing newlines
 */
static char *_read_authfile(const char *auth_dir, const char *base)
{
	char *fname = NULL;
	int fd = -1;
	struct stat statbuf;
	size_t siz = 0;
	char *buf = NULL;

	fname = xstrdup_printf("%s/%s", auth_dir, base);
	fd = open(fname, O_RDONLY);
	if (fd == -1) {
		error("Couldn't open %s: %m", fname);
		goto rwfail;
	}
	if (fstat(fd, &statbuf) == -1) {
		error("fstat failed on %s: %m", fname);
		goto rwfail;
	}
	siz = statbuf.st_size;
	buf = xmalloc(siz + 1);
	safe_read(fd, buf, siz);
	while (siz > 0 && buf[siz - 1] == '\n')
		siz--;
	buf[siz] = '\0';

	xfree(fname);
	close(fd);
	return buf;

rwfail:
	xfree(fname);
	xfree(buf);
	close(fd);
	return NULL;
}

/*
 * Clear OAUTH authentication header
 */
static void _clear_auth_header(slingshot_rest_conn_t *conn)
{
	if (conn->auth.u.oauth.auth_cache) {
		memset((void *)conn->auth.u.oauth.auth_cache, 0,
		       strlen(conn->auth.u.oauth.auth_cache));
		xfree(conn->auth.u.oauth.auth_cache);
	}
}

/*
 * If needed, access a token service to get an OAUTH2 auth token;
 * on success, cache the authorization header in conn->auth.u.oauth.auth_cache,
 * add the header to *headers and return true;
 * if 'cache_use' is set, return the cached auth_header if set
 */
static bool _get_auth_header(slingshot_rest_conn_t *conn,
			     struct curl_slist **headers, bool cache_use)
{
	bool retval = false;
	char *client_id = NULL;
	char *client_secret = NULL;
	char *url = NULL;
	slingshot_rest_conn_t token_conn = { 0 };  /* Conn to token service */
	char *req = NULL, *response_str = NULL;
	json_object *respjson = NULL;
	json_object *tokjson = NULL;
	const char *token = NULL;
	long status = 0;
	struct curl_slist *newhdrs = NULL;

	/* Just return if not OAUTH */
	if (conn->auth.auth_type != SLINGSHOT_AUTH_OAUTH)
		return true;

	/* Use token service to get token unless cache_use set (or 1st call) */
	if (!cache_use || !conn->auth.u.oauth.auth_cache) {

		/* Get a new token from the token service */
		_clear_auth_header(conn);

		/* Get the token URL and client_{id,secret}, create request */
		url = _read_authfile(conn->auth.auth_dir,
				     SLINGSHOT_AUTH_OAUTH_ENDPOINT_FILE);
		if (!url)
			goto err;

		client_id = _read_authfile(conn->auth.auth_dir,
					   SLINGSHOT_AUTH_OAUTH_CLIENT_ID_FILE);
		if (!client_id)
			goto err;
		client_secret =
			_read_authfile(conn->auth.auth_dir,
				       SLINGSHOT_AUTH_OAUTH_CLIENT_SECRET_FILE);
		if (!client_secret)
			goto err;
		req = xstrdup_printf("grant_type=client_credentials"
				     "&client_id=%s&client_secret=%s"
				     "&scope=openid",
				     client_id, client_secret);

		if (slurm_curl_request(req, url, NULL, NULL, conn->mtls.ca_path,
				       conn->mtls.cert_path,
				       conn->mtls.key_path, NULL,
				       SLINGSHOT_TOKEN_TIMEOUT, &response_str,
				       &status, HTTP_REQUEST_POST, false,
				       conn->mtls.enabled))
			goto err;

		/* Decode response into JSON */
		if (response_str && response_str[0]) {
			enum json_tokener_error jerr;
			respjson = json_tokener_parse_verbose(response_str,
							      &jerr);
			if (respjson == NULL) {
				error("Couldn't decode %s response: %s (data '%s')",
				      conn->name, json_tokener_error_desc(jerr),
				      response_str);
				goto err;
			}
		} else {
			error("%s No http response received. Status: %ld",
			      conn->name, status);
			goto err;
		}

		/* On a successful response, get the access_token out of it */
		if (status == HTTP_OK) {
			debug("%s POST %s successful", token_conn.name, url);
		} else {
			_log_rest_detail(token_conn.name, "POST", url,
					 respjson, status);
			goto err;
		}

		/* Create an authentication header from the access_token */
		if (!json_object_object_get_ex(respjson, "access_token",
					       &tokjson) ||
		    !(token = json_object_get_string(tokjson))) {
			error("Couldn't get auth token from OAUTH service: json='%s'",
			      json_object_to_json_string(respjson));
			goto err;
		}
		conn->auth.u.oauth.auth_cache = xstrdup_printf(
					"Authorization: Bearer %s", token);
	}

	/* Append new header and return */
	if (!headers) {
		retval = true;
	} else if (!(newhdrs = curl_slist_append(*headers,
					conn->auth.u.oauth.auth_cache))) {
		error("curl_slist_append couldn't add OAUTH header");
		/* retval already false */
	} else {
		*headers = newhdrs;
		retval = true;
	}

err:
	xfree(client_id);
	xfree(client_secret);
	xfree(url);
	slingshot_rest_destroy_connection(&token_conn);
	xfree(req);
	xfree(response_str);
	json_object_put(respjson);
	return retval;
}
