/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/***************************************************************************
 * Copyright (C) 2017-2026 ZmartZone Holding BV
 * All rights reserved.
 *
 * DISCLAIMER OF WARRANTIES:
 *
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
 */

#include "cfg/dir.h"
#include "handle/handle.h"
#include "metrics.h"
#include "mod_auth_openidc.h"
#include "util/util.h"
#include "util/util_cfg.h"

static int oidc_session_management_iframe_op(request_rec *r, const char *check_session_iframe) {
	oidc_debug(r, "enter");
	oidc_http_hdr_out_location_set(r, check_session_iframe);
	return HTTP_MOVED_TEMPORARILY;
}

static int oidc_session_management_iframe_rp(request_rec *r, const oidc_cfg_t *c, const oidc_session_t *session,
					     const char *client_id, const char *check_session_iframe) {

	oidc_debug(r, "enter");

	static const char java_script_tmpl[] =
	    "    <script type=\"text/javascript\">\n"
	    "      var targetOrigin  = '%s';\n"
	    "      var clientId  = '%s';\n"
	    "      var sessionId  = '%s';\n"
	    "      var loginUrl  = '%s';\n"
	    "      var message = clientId + ' ' + sessionId;\n"
	    "\t   var timerID;\n"
	    "\n"
	    "      function checkSession() {\n"
	    "        console.debug('checkSession: posting ' + message + ' to ' + targetOrigin);\n"
	    "        var win = window.parent.document.getElementById('%s').contentWindow;\n"
	    "        win.postMessage( message, targetOrigin);\n"
	    "      }\n"
	    "\n"
	    "      function setTimer() {\n"
	    "        checkSession();\n"
	    "        timerID = setInterval('checkSession()', %d);\n"
	    "      }\n"
	    "\n"
	    "      function receiveMessage(e) {\n"
	    "        console.debug('receiveMessage: ' + e.data + ' from ' + e.origin);\n"
	    "        if (e.origin !== targetOrigin ) {\n"
	    "          console.debug('receiveMessage: cross-site scripting attack?');\n"
	    "          return;\n"
	    "        }\n"
	    "        if (e.data != 'unchanged') {\n"
	    "          clearInterval(timerID);\n"
	    "          if (e.data == 'changed' && sessionId == '' ) {\n"
	    "\t\t\t // 'changed' + no session: enforce a login (if we have a login url...)\n"
	    "            if (loginUrl != '') {\n"
	    "              window.top.location.replace(loginUrl);\n"
	    "            }\n"
	    "\t\t   } else {\n"
	    "              // either 'changed' + active session, or 'error': enforce a logout\n"
	    "              window.top.location.replace('%s?logout=' + encodeURIComponent(window.top.location.href));\n"
	    "          }\n"
	    "        }\n"
	    "      }\n"
	    "\n"
	    "      window.addEventListener('message', receiveMessage, false);\n"
	    "\n"
	    "    </script>\n";

	/* determine the origin for the check_session_iframe endpoint */
	const char *origin = apr_pstrdup(r->pool, check_session_iframe);
	apr_uri_t uri;
	if (apr_uri_parse(r->pool, check_session_iframe, &uri) != APR_SUCCESS) {
		oidc_error(r, "could not parse the check_session_iframe URL: %s", check_session_iframe);
		return HTTP_INTERNAL_SERVER_ERROR;
	}
	char *p = (uri.path != NULL) ? _oidc_strstr(origin, uri.path) : NULL;
	if (p != NULL)
		*p = '\0';

	/* the element identifier for the OP iframe */
	const char *op_iframe_id = "openidc-op";

	/* restore the OP session_state from the session */
	const char *session_state = oidc_session_get_session_state(r, session);
	if (session_state == NULL) {
		oidc_warn(
		    r, "no session_state found in the session; the OP does probably not support session management!?");
	}

	char *s_poll_interval = NULL;
	oidc_util_url_parameter_get(r, "poll", &s_poll_interval);
	int poll_interval = _oidc_str_to_int(s_poll_interval, 0);
	if ((poll_interval <= 0) || (poll_interval > 3600 * 24))
		poll_interval = 3000;

	char *login_uri = NULL;
	char *error_str = NULL;
	char *error_description = NULL;
	oidc_util_url_parameter_get(r, "login_uri", &login_uri);
	if ((login_uri != NULL) && (oidc_validate_redirect_url(r, c, login_uri, OIDC_REDIRECT_URL_SAME_HOST, &error_str,
							       &error_description) == FALSE)) {
		return HTTP_BAD_REQUEST;
	}

	const char *redirect_uri = oidc_util_url_redirect_uri(r, c);

	const char *java_script =
	    apr_psprintf(r->pool, java_script_tmpl, oidc_util_html_javascript_escape(r->pool, origin),
			 oidc_util_html_javascript_escape(r->pool, client_id),
			 session_state ? oidc_util_html_javascript_escape(r->pool, session_state) : "",
			 login_uri ? oidc_util_html_javascript_escape(r->pool, login_uri) : "", op_iframe_id,
			 poll_interval, oidc_util_html_javascript_escape(r->pool, redirect_uri));

	return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, NULL, java_script, "setTimer()", NULL);
}

/*
 * handle session management request
 */
int oidc_session_management(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) {
	char *cmd = NULL;
	const char *id_token_hint = NULL;
	oidc_provider_t *provider = NULL;

	/* get the command passed to the session management handler */
	oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_SESSION, &cmd);
	if (cmd == NULL) {
		oidc_error(r, "session management handler called with no command");
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* see if this is a local logout during session management */
	if (_oidc_strcmp("logout", cmd) == 0) {
		oidc_debug(
		    r,
		    "[session=logout] calling oidc_handle_logout_request because of session mgmt local logout call.");
		return oidc_logout_request(r, c, session, oidc_util_url_abs(r, c, oidc_cfg_default_slo_url_get(c)),
					   TRUE);
	}

	if ((oidc_get_provider_from_session(r, c, session, &provider) == FALSE) &&
	    ((oidc_provider_static_config(r, c, &provider) == FALSE) || (provider == NULL)))
		return HTTP_NOT_FOUND;

	/* see if this is a request for the OP iframe */
	if (_oidc_strcmp("iframe_op", cmd) == 0) {
		if (oidc_cfg_provider_check_session_iframe_get(provider) != NULL) {
			return oidc_session_management_iframe_op(r,
								 oidc_cfg_provider_check_session_iframe_get(provider));
		}
		return HTTP_NOT_FOUND;
	}

	/* see if this is a request for the RP iframe */
	if (_oidc_strcmp("iframe_rp", cmd) == 0) {
		if ((oidc_cfg_provider_client_id_get(provider) != NULL) &&
		    (oidc_cfg_provider_check_session_iframe_get(provider) != NULL)) {
			return oidc_session_management_iframe_rp(r, c, session,
								 oidc_cfg_provider_client_id_get(provider),
								 oidc_cfg_provider_check_session_iframe_get(provider));
		}
		oidc_debug(r, "iframe_rp command issued but no client (%s) and/or no check_session_iframe (%s) set",
			   oidc_cfg_provider_client_id_get(provider),
			   oidc_cfg_provider_check_session_iframe_get(provider));
		return HTTP_NOT_FOUND;
	}

	/* see if this is a request check the login state with the OP */
	if (_oidc_strcmp("check", cmd) == 0) {
		id_token_hint = oidc_session_get_idtoken(r, session);
		/*
		 * Reuse settings persisted for the original protected path. Older sessions lack them, so fall
		 * back to the current redirect URI's per-path settings.
		 */
		const char *auth_request_params = oidc_session_get_path_auth_request_params(r, session);
		if (auth_request_params == NULL)
			auth_request_params = oidc_cfg_dir_path_auth_request_params_get(r);
		const char *path_scope = oidc_session_get_path_scope(r, session);
		if (path_scope == NULL)
			path_scope = oidc_cfg_dir_path_scope_get(r);
		return oidc_request_authenticate_user(
		    r, c, provider, apr_psprintf(r->pool, "%s?session=iframe_rp", oidc_util_url_redirect_uri(r, c)),
		    NULL, id_token_hint, "none", auth_request_params, path_scope);
	}

	/* handle failure in fallthrough */
	oidc_error(r, "unknown command: %s", cmd);

	return HTTP_INTERNAL_SERVER_ERROR;
}
