/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed 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.
 */
package org.opengion.hayabusa.filter;

import org.opengion.hayabusa.common.HybsSystem;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;

import org.opengion.fukurou.security.URLHashMap;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.FileUtil;								// 6.4.5.2 (2016/05/06)
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;		// 6.1.0.0 (2014/12/26) refactoring
import org.opengion.fukurou.system.HybsConst;							// 6.4.5.2 (2016/05/06)

/**
 * URLHashFilter は、Filter インターフェースを継承した URLチェッククラスです。
 * web.xml で filter 設定することにより、処理を開始します。
 * filter 処理は、設定レベルとURLの飛び先により処理方法が異なります。
 * このフィルターでは、ハッシュ化/暗号化ではなく、アドレスに戻す作業になります。
 * 内部URLの場合はハッシュ化、外部URLの場合は暗号化に適用されます。
 *
 * 基本的には、外部へのURLでエンジンシステムへ飛ばす場合は、暗号化になります。
 * 内部へのURLは、基本的に、パラメータのみ暗号化を行います。なお、直接画面IDを
 * 指定して飛ばす場合を、止めるかどうかは、設定レベルに依存します。
 *
 * フィルターの設定レベルは、システムリソースの URL_ACCESS_SECURITY_LEVEL 変数で
 * 設定します。
 * なお、各レベル共通で、戻し処理はレベルに関係なく実行されます。
 *   レベル０：なにも制限はありません。
 *   レベル１：Referer チェックを行います。つまり、URLを直接入力しても動作しません。
 *             ただし、Refererが付いてさえいれば、アクセス許可を与えます。
 *             Referer 無しの場合でも、URLにパラメータが存在しない、または、
 *             アドレスがハッシュ化/暗号化されている場合は、アクセスを許可します。
 *             レベル１の場合、ハッシュ戻し/復号化処理は行います。あくまで、ハッシュ化
 *             暗号化されていない場合でも、Refererさえあれば、許可するということです。
 *             (パラメータなし or ハッシュあり or Refererあり の場合、許可)
 *   レベル２：フィルター処理としては、レベル１と同じです。
 *             異なるのは、URLのハッシュ化/暗号化処理を、外部URLに対してのみ行います。
 *             (パラメータなし or ハッシュあり or Refererあり の場合、許可)
 *   レベル３：URLのパラメータがハッシュ化/暗号化されている必要があります。
 *             レベル１同様、URLにパラメータが存在しない場合は、アクセスを許可します。
 *             レベル１と異なるのは、パラメータは必ずハッシュ化か、暗号化されている
 *             必要があるということです。(内部/外部問わず)
 *             (パラメータなし or ハッシュあり の場合、許可)
 *   それ以外：アクセスを停止します。
 *
 * フィルターに対してweb.xml でパラメータを設定します。
 *   ・filename   :停止時メッセージ表示ファイル名(例:/jsp/custom/refuseAccess.html)
 *   ・initPage   :最初にアクセスされる初期画面アドレス(初期値:/jsp/index.jsp)
 *   ・debug      :デバッグメッセージの表示(初期値:false)
 *
 * 【WEB-INF/web.xml】
 *     &lt;filter&gt;
 *         &lt;filter-name&gt;URLHashFilter&lt;/filter-name&gt;
 *         &lt;filter-class&gt;org.opengion.hayabusa.filter.URLHashFilter&lt;/filter-class&gt;
 *         &lt;init-param&gt;
 *             &lt;param-name&gt;filename&lt;/param-name&gt;
 *             &lt;param-value&gt;/jsp/custom/refuseAccess.html&lt;/param-value&gt;
 *         &lt;/init-param&gt;
 *          &lt;init-param&gt;
 *              &lt;param-name&gt;initPage&lt;/param-name&gt;
 *              &lt;param-value&gt;/jsp/index.jsp&lt;/param-value&gt;
 *          &lt;/init-param&gt;
 *          &lt;init-param&gt;
 *              &lt;param-name&gt;debug&lt;/param-name&gt;
 *              &lt;param-value&gt;false&lt;/param-value&gt;
 *          &lt;/init-param&gt;
 *     &lt;/filter&gt;
 *
 *     &lt;filter-mapping&gt;
 *         &lt;filter-name&gt;URLHashFilter&lt;/filter-name&gt;
 *         &lt;url-pattern&gt;*.jsp&lt;/url-pattern&gt;
 *     &lt;/filter-mapping&gt;
 *
 * @og.group フィルター処理
 *
 * @og.rev 5.2.2.0 (2010/11/01) 新規追加
 *
 * @version  5.2.2.0 (2010/11/01)
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.6,
 */
public final class URLHashFilter implements Filter {
	private static final String REQ_KEY = HybsSystem.URL_HASH_REQ_KEY ;

	private static final int ACCS_LVL = HybsSystem.sysInt( "URL_ACCESS_SECURITY_LEVEL" );

	private String		initPage	= "/jsp/index.jsp";
	private String		filename	= "jsp/custom/refuseAccess.html" ;			// 6.3.8.3 (2015/10/03) アクセス拒否時メッセージ表示ファイル名
	private boolean		isDebug		;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public URLHashFilter() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * フィルター処理本体のメソッドです。
	 *
	 * @og.rev 5.3.0.0 (2010/12/01) 文字化け対策として、setCharacterEncoding を実行する。
	 * @og.rev 6.3.8.3 (2015/10/03) アクセス拒否を示すメッセージファイルの内容を取り出します。
	 *
	 * @param	request		ServletRequestオブジェクト
	 * @param	response	ServletResponseオブジェクト
	 * @param	chain		FilterChainオブジェクト
	 * @throws IOException 入出力エラーが発生したとき
	 * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
	 */
	@Override	// Filter
	public void doFilter( final ServletRequest request,
							final ServletResponse response,
							final FilterChain chain ) throws IOException, ServletException {

		final HttpServletRequest req = (HttpServletRequest)request ;
		req.setCharacterEncoding( "UTF-8" );	// 5.3.0.0 (2010/12/01)

		if( isValidAccess( req ) ) {
			final String h_r = req.getParameter( REQ_KEY );
			// ハッシュ化キーが存在する。
			// 6.0.2.5 (2014/10/31) refactoring: findBugs:未チェック/未確認のキャスト対応。
			if( h_r != null && response instanceof HttpServletResponse ) {
				final HttpServletResponse resp = ((HttpServletResponse)response);
				final String qu = URLHashMap.getValue( h_r );
				// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
				// キーに対する実アドレスが存在しない。(行き先無しのケース)
				if( qu == null ) {
					final String url = resp.encodeRedirectURL( initPage );
					resp.sendRedirect( url );
				}
				// キーに対する実アドレスが存在する。
				else {
					final String requestURI = req.getRequestURI();		// /gf/jsp/index.jsp など
					final String cntxPath   = req.getContextPath();		// /gf など
					// 自分自身のコンテキストと同じなので、forward できる。
					if( requestURI.startsWith( cntxPath ) ) {
						final String url = requestURI.substring(cntxPath.length()) + "?" + qu ;
						final RequestDispatcher rd = request.getRequestDispatcher( url );
						rd.forward( request,response );
					}
					// そうでない場合、リダイレクトする。
					else {
						final String url = resp.encodeRedirectURL( requestURI + "?" + qu );
						resp.sendRedirect( url );
					}
				}
			}
			// ハッシュ化キーが存在しない。
			else {
				chain.doFilter(request, response);
			}
		}
		else {
			// アクセス拒否を示すメッセージファイルの内容を出力する。
			response.setContentType( "text/html; charset=UTF-8" );
			final PrintWriter out = response.getWriter();
			out.println( refuseMsg() );							// 6.3.8.3 (2015/10/03)
			out.flush();
		}
	}

	/**
	 * フィルターの初期処理メソッドです。
	 *
	 * フィルターに対してweb.xml で初期パラメータを設定します。
	 *   ・filename   :停止時メッセージ表示ファイル名
	 *   ・initPage   :最初にアクセスされる初期画面アドレス(初期値:/jsp/index.jsp)
	 *   ・debug      :デバッグメッセージの表示(初期値:false)
	 *
	 * @og.rev 5.7.3.2 (2014/02/28) Tomcat8 対応。getRealPath( "/" ) の互換性のための修正。
	 * @og.rev 6.2.4.1 (2015/05/22) REAL_PATH 対応。realPath は、HybsSystem経由で、取得する。
	 * @og.rev 6.3.8.3 (2015/10/03) filenameの初期値設定。
	 *
	 * @param config FilterConfigオブジェクト
	 */
	@Override	// Filter
	public void init( final FilterConfig config ) {
		initPage = StringUtil.nval( config.getInitParameter("initPage"), initPage );
		isDebug	 = StringUtil.nval( config.getInitParameter("debug")   , isDebug  );

		filename = HybsSystem.getRealPath() + StringUtil.nval( config.getInitParameter("filename") , filename );	// 6.3.8.3 (2015/10/03)
	}

	/**
	 * フィルターの終了処理メソッドです。
	 *
	 */
	@Override	// Filter
	public void destroy() {
		// ここでは処理を行いません。
	}

	/**
	 * アクセス拒否を示すメッセージ内容。
	 *
	 * @og.rev 6.3.8.3 (2015/10/03) アクセス拒否を示すメッセージファイルの内容を取り出します。
	 * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
	 * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
	 *
	 * @return アクセス拒否を示すメッセージファイルの内容
	 */
	private String refuseMsg() {
		// アクセス拒否を示すメッセージファイルの内容を管理する FileString オブジェクトを構築する。

		// 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
		return FileUtil.getValue( filename , HybsConst.UTF_8 );			// 6.4.5.2 (2016/05/06)
	}

	/**
	 * フィルターの内部状態をチェックするメソッドです。
	 *
	 * 判定条件は、URL_ACCESS_SECURITY_LEVEL 変数 に応じて異なります。
	 *     レベル０：なにも制限はありません。
	 *     レベル１：Referer チェックを行います。つまり、URLを直接入力しても動作しません。
	 *     レベル２：URLのハッシュ化/暗号化処理を、外部URLに対してのみ行います。(チェックは、レベル１と同等)
	 *     レベル３：URLのパラメータがハッシュ化/暗号化されている必要があります。
	 *     それ以外：アクセスを停止します。
	 *
	 * @param request HttpServletRequestオブジェクト
	 *
	 * @return	(true:許可  false:拒否)
	 */
	private boolean isValidAccess( final HttpServletRequest request ) {
		if( ACCS_LVL == 0 )      { return true;  }	// レベル０：無条件アクセス

		final String httpReferer = request.getHeader( "Referer" );
		final String requestURI  = request.getRequestURI();
		final String queryString = request.getQueryString();
		final String hashVal     = request.getParameter( REQ_KEY );

		if( isDebug ) {
			System.out.println( "URLHashFilter#httpReferer = " + httpReferer );
			System.out.println( "URLHashFilter#requestURI  = " + requestURI  );
		}

		// 基準となる許可：パラメータなし or ハッシュありの場合
		final boolean flag2 = queryString == null || hashVal != null ;

		// レベル１,２：パラメータなし or ハッシュあり or Refererあり の場合、許可
		if( ACCS_LVL == 1 || ACCS_LVL == 2 ) {
			return flag2 || httpReferer != null ;
		}

		// レベル３：パラメータなし or ハッシュありの場合、許可
		if( ACCS_LVL == 3 ) {
			final String cntxPath = request.getContextPath();		// /gf など
			// 特別処置
			return flag2 ||
					  requestURI.equalsIgnoreCase( initPage )            ||
					  requestURI.startsWith( cntxPath + "/jsp/menu/"   ) ||
					  requestURI.startsWith( cntxPath + "/jsp/custom/" ) ||
					  requestURI.startsWith( cntxPath + "/jsp/common/" ) ;
		}

		return false;	// それ以外：無条件拒否
	}

	/**
	 * 内部状態を文字列で返します。
	 *
	 * @return	このクラスの文字列表示
	 * @og.rtnNotNull
	 */
	@Override	// Object
	public String toString() {
		final StringBuilder sb = new StringBuilder( BUFFER_MIDDLE )
			.append( this.getClass().getCanonicalName() ).append( " : ")
			.append( "initPage = [" ).append( initPage ).append( "] , ")
			.append( "isDebug  = [" ).append( isDebug  ).append( "]");
		return sb.toString();
	}
}
