/*
 * 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 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.http.HttpServletRequest;

import org.opengion.fukurou.security.HybsCryptography;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.hayabusa.common.HybsSystem;
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;		// 6.1.0.0 (2014/12/26) refactoring
import org.opengion.fukurou.system.ThrowUtil;							// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.HybsConst;							// 6.4.5.2 (2016/05/06)

import org.opengion.fukurou.util.FileUtil;								// 6.4.5.2 (2016/05/06)

/**
 * URLCheckFilter は、Filter インターフェースを継承した URLチェッククラスです。
 * web.xml で filter 設定することにより、該当のリソースに対して、og:linkタグで、
 * useURLCheck="true"が指定されたリンクURL以外を拒否することができます。
 * また、og:linkタグを経由した場合でも、リンクの有効期限を設定することで、
 * リンクURLの漏洩に対しても、一定時間の経過を持って、アクセスを拒否することができます。
 * また、リンク時にユーザー情報も埋め込んでいますので(初期値は、ログインユーザー)、
 * リンクアドレスが他のユーザーに知られた場合でも、アクセスを拒否することができます。
 *
 * システムリソースの「URL_CHECK_CRYPT」で暗号復号化のキーを指定可能です。
 * 指定しない場合はデフォルトのキーが利用されます。
 * キーの形式はHybsCryptographyに従います。
 *
 * フィルターに対してweb.xml でパラメータを設定します。
 *   ・filename :停止時メッセージ表示ファイル名
 *   ・ignoreURL:暗号化されたURLのうち空白に置き換える接頭文字列を指定します。
 *              外部からアクセスしたURLがロードバランサで内部向けURLに変換されてチェックが動作しないような場合に
 *              利用します。https://wwwX.のように指定します。通常は設定しません。
 *   ・debug	:標準出力に状況を表示します(true/false) 5.10.18.0 (2019/11/29)
 *   ・ommitURL :正規表現で、URLチェックを行わないパターンを記載します 5.10.18.0 (2019/11/29)
 *   ・ommitReferer:ドメイン(ホスト名)を指定すると、このRefererを持つものはURLチェックを行いません。 5.10.18.0 (2019/11/29)
 *              内部の遷移はチェックを行わず、外部からのリンクのみチェックを行う場合に利用します。
 *   ・ignoreRelative:trueにすると暗号化されたURLのうち、相対パスの箇所(../)を削除して評価します。  5.10.18.0 (2019/11/29)
 *
 * 【WEB-INF/web.xml】
 *     &lt;filter&gt;
 *         &lt;filter-name&gt;URLCheckFilter&lt;/filter-name&gt;
 *         &lt;filter-class&gt;org.opengion.hayabusa.filter.URLCheckFilter&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;/filter&gt;
 *
 *     &lt;filter-mapping&gt;
 *         &lt;filter-name&gt;URLCheckFilter&lt;/filter-name&gt;
 *         &lt;url-pattern&gt;/jsp/*&lt;/url-pattern&gt;
 *     &lt;/filter-mapping&gt;
 *
 * @og.group フィルター処理
 *
 * @version  4.0
 * @author   Hiroki Nakamura
 * @since    JDK5.0,
 */
public final class URLCheckFilter implements Filter {

	private static final HybsCryptography HYBS_CRYPTOGRAPHY
						= new HybsCryptography( HybsSystem.sys( "URL_CHECK_CRYPT" ) ); // 5.8.8.0 (2015/06/05)

	private static final String  USERID_HEADER = HybsSystem.sys( "USERID_HEADER_NAME" ); // 5.10.18.0 (2019/11/29)

	private String	filename	= "jsp/custom/refuseAccess.html" ;			// 6.3.8.3 (2015/10/03) アクセス拒否時メッセージ表示ファイル名
	private boolean	isDebug		;
	private boolean	isDecode	= true;			// 5.4.5.0(2012/02/28) URIDecodeするかどうか

	private String	ignoreURL	;				// 5.8.6.1 (2015/04/17) 飛んできたcheckURLから取り除くURL文字列
	private String	ignoreRelative = "false";	// 5.10.18.1 (2019/12/09) 相対パスの../を無視する
	private String	ommitURL	;				// 5.10.11.0 (2019/05/03) URLチェックを行わないURLの正規表現
	private String	ommitReferer;				// 5.10.11.0 (2019/05/03) URLチェックを行わないドメイン

	private String encoding = "utf-8";	// 5.10.12.4 (2019/06/21) 日本語対応

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

	/**
	 * フィルター処理本体のメソッドです。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) new BufferedReader … を、FileUtil.getBufferedReader … に変更。
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
	 * @og.rev 6.3.8.3 (2015/10/03) アクセス拒否を示すメッセージファイルの内容を取り出します。
	 * @og.rev 5.10.12.4 (2019/06/21) 日本語対応(encoding指定)
	 * @og.rev 5.10.16.1 (2019/10/11) デバッグ追加
	 *
	 * @param	request		ServletRequestオブジェクト
	 * @param	response	ServletResponseオブジェクト
	 * @param	chain		FilterChainオブジェクト
	 * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
	 */
	@Override	// Filter
	public void doFilter( final ServletRequest request,
							final ServletResponse response,
							final FilterChain chain ) throws IOException, ServletException {

		if( !isValidAccess( request ) ) {
			if( isDebug ) {
				System.out.println( "  check NG... " ); // 5.10.16.1
			}

			response.setContentType( "text/html; charset=UTF-8" );
			final PrintWriter out = response.getWriter();
			out.println( refuseMsg() );							// 6.3.8.3 (2015/10/03)
			out.flush();
			return;
		}

		request.setAttribute( "RequestEncoding", encoding );	// 5.10.12.1 (2019/06/21) リクエスト変数で送信しておく

		chain.doFilter(request, response);
	}

	/**
	 * フィルターの初期処理メソッドです。
	 *
	 * フィルターに対してweb.xml で初期パラメータを設定します。
	 *   ・filename  :停止時メッセージ表示ファイル名(初期:jsp/custom/refuseAccess.html)
	 *   ・decode    :URLデコードを行ってチェックするか(初期:true)
	 *   ・ignoreURL :暗号化されたURLのうち空白に置き換える接頭文字列を指定します(初期:null)
	 *   ・debug     :URLデコードを行ってチェックするか(初期:false)
	 *
	 * @og.rev 5.4.5.0 (2102/02/28)
	 * @og.rev 5.7.3.2 (2014/02/28) Tomcat8 対応。getRealPath( "/" ) の互換性のための修正。
	 * @og.rev 5.8.6.1 (2015/04/17) DMZのURL変換対応
	 * @og.rev 6.2.4.1 (2015/05/22) REAL_PATH 対応。realPath は、HybsSystem経由で、取得する。
	 * @og.rev 6.3.8.3 (2015/10/03) filenameの初期値設定。
	 * @og.rev 5.10.11.0 (2019/05/03) ommitURL,ommitReferer
	 * @og.rev 5.10.12.4 (2019/06/21) encoding
	 * @og.rev 5.10.18.1 (2019/12/09) 相対パス対応
	 *
	 * @param filterConfig FilterConfigオブジェクト
	 */
	@Override	// Filter
	public void init(final FilterConfig filterConfig) {

		filename  = HybsSystem.getRealPath() + StringUtil.nval( filterConfig.getInitParameter("filename") , filename );	// 6.3.8.3 (2015/10/03)
		isDecode  = StringUtil.nval( filterConfig.getInitParameter("decode"), true );	// 5.4.5.0(2012/02/28)
		ignoreURL = filterConfig.getInitParameter("ignoreURL");							// 5.8.6.1 (2015/04/17)
		ignoreRelative = filterConfig.getInitParameter("ignoreRelative");				// 5.10.18.1 (2019/12/09)
		isDebug   = StringUtil.nval( filterConfig.getInitParameter("debug"), false );

		ommitURL     = filterConfig.getInitParameter("ommitURL");						// 5.10.11.0 (2019/05/03)
		ommitReferer = filterConfig.getInitParameter("ommitReferer");					// 5.10.11.0 (2019/05/03)

		encoding = StringUtil.nval( filterConfig.getInitParameter("encoding"), encoding ); // 5.10.12.4 (2019/06/21)
	}

	/**
	 * フィルターの終了処理メソッドです。
	 *
	 */
	@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)
	}

	/**
	 * フィルターの内部状態をチェックするメソッドです。
	 *
	 * @og.rev 5.4.5.0 (2012/02/28) Decode
	 * @og.rev 5.8.6.1 (2015/04/17) DMZのURL変換対応
	 * @og.rev 5.8.8.2 (2015/07/17) マルチバイト対応追加
	 * @og.rev 6.3.8.4 (2015/10/09) デバッグメッセージの追加
	 * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
	 * @og.rev 5.10.11.0 (2019/05/03) ommitURL,ommitReferer
	 * @or.rev 5.10.16 (2019/10/11) デバッグ追加
	 * @og.rev 5.10.18.0 (2019/11/29) ヘッダ認証
	 * @og.rev 5.10.18.1 (2019/12/09) 相対パス対応
	 *
	 * @param request ServletRequestオブジェクト
	 *
	 * @return	(true:許可  false:拒否)
	 */
	private boolean isValidAccess( final ServletRequest request ) {
		// 6.3.8.4 (2015/10/09) デバッグメッセージの追加
		if( isDebug ) {
			System.out.println( ((HttpServletRequest)request).getRequestURI() );
		}

		// 5.10.11.0 (2019/05/03) データ取得位置変更
		String reqStr = ((HttpServletRequest)request).getRequestURL().toString();

		// 5.10.11.0 referer判定追加
		// 入っている場合はtrueにする。
		final String referer  = ((HttpServletRequest)request).getHeader("REFERER");
		if(referer != null && ommitReferer != null && referer.indexOf( ommitReferer ) >= 0 ) {
			if( isDebug ) {
				System.out.println("URLCheck ommitRef"+reqStr);
			}
			return true;
		}

		// リクエスト変数をURLに追加
		final String queryStr = ((HttpServletRequest)request).getQueryString();
		reqStr = reqStr + (queryStr != null ? "?" + queryStr : "");

		// 5.10.11.0 ommitURL追加
		// ommitに合致する場合はtrueにする。
		if(ommitURL != null && reqStr.matches( ommitURL )) {
			if( isDebug ) {
				System.out.println("URLCheck ommitURL"+reqStr);
			}
			return true;
		}

		String checkKey = request.getParameter( HybsSystem.URL_CHECK_KEY );
		if( checkKey == null || checkKey.isEmpty() ) {
			if( isDebug ) {
//				System.out.println( "  check NG [ No Check Key ]" );
				System.out.println( "  check NG [ No Check Key ] = " + reqStr ); // 5.10.16.1 (2019/10/11) reqStr追加
			}
			return false;
		}

		boolean rtn = false;
		try {
			checkKey = HYBS_CRYPTOGRAPHY.decrypt( checkKey ).replace( "&amp;", "&" );

			if( isDebug ) {
				System.out.println( "  checkKey=" + checkKey );
			}

			// 5.8.6.1 (2015/04/17) DMZのURL変換対応 (ちょっと整理しておきます)

			final int tmAd = checkKey.lastIndexOf( ",time=" );
			final int usAd = checkKey.lastIndexOf( ",userid=" );

			String url = checkKey.substring( 0 , tmAd );
			final long  time    = Long.parseLong( checkKey.substring( tmAd + 6, usAd ) );
			final String userid = checkKey.substring( usAd + 8 );

			// 4.3.8.0 (2009/08/01)
			final String[] userArr = StringUtil.csv2Array( userid );

			// 5.8.6.1 (2015/04/17)ignoreURL対応
			if( ignoreURL != null && ignoreURL.length()>0 && url.indexOf( ignoreURL ) == 0 ){
				url = url.substring( ignoreURL.length() );
			}

			// 5.10.18.1 (2019/12/09) ../を削除
			if ( "true".equals( ignoreRelative ) ) {
				url = url.replaceAll( "\\.\\./", "" );
			}

			if( isDebug ) {
				System.out.println( "   [ignoreURL]=" + ignoreURL );	// 2015/04/17 (2015/04/17)
				System.out.println( "   [ignoreRelative]=" + ignoreRelative );
				System.out.println( "   [url]      =" + url );
				System.out.println( "   [vtime]    =" + time );
				System.out.println( "   [userid]   =" + userid );
			}

//			String reqStr =  ((HttpServletRequest)request).getRequestURL().toString() + "?" + ((HttpServletRequest)request).getQueryString();
			// 5.4.5.0 (2012/02/28) URLDecodeを行う
			if( isDecode ){
				if( isDebug ) {
					System.out.println( "[BeforeURIDecode]="+reqStr );
				}
				reqStr = StringUtil.urlDecode( reqStr );
				url = StringUtil.urlDecode( url );		// 5.8.8.2 (2015/07/17)
			}
			reqStr = reqStr.substring( 0, reqStr.lastIndexOf( HybsSystem.URL_CHECK_KEY ) -1 );
			//	String reqStr =  ((HttpServletRequest)request).getRequestURL().toString();
//			final String reqUser = ((HttpServletRequest)request).getRemoteUser();
			String reqUser = ((HttpServletRequest)request).getRemoteUser() ; // 5.10.18.0 (2019/11/29)
			if( USERID_HEADER != null && USERID_HEADER.length() > 0 && ( reqUser == null || reqUser.length() == 0) ) {
				reqUser = ((HttpServletRequest)request).getHeader( USERID_HEADER );
			}

			if( isDebug ) {
				System.out.println( "   [reqURL] =" + reqStr );
				System.out.println( "   [ctime]  =" + System.currentTimeMillis() );
				System.out.println( "   [reqUser]=" + reqUser );
				System.out.println( " endWith=" + reqStr.endsWith( url ) );
				System.out.println( " times=" + (System.currentTimeMillis() - time) );
				System.out.println( " [userArr.length]=" + userArr.length );
			}

			if( reqStr.endsWith( url )
					&& System.currentTimeMillis() - time < 0
					&& userArr != null && userArr.length > 0 ) {
				// 4.3.8.0 (2009/08/01)
				for( int i=0; i<userArr.length; i++ ) {
					if( isDebug ) {
						System.out.println( " [userArr] =" + userArr[i] ); // 5.10.16.1
					}
					if( "*".equals( userArr[i] ) || reqUser.equals( userArr[i] ) ) {
						rtn = true;
						if( isDebug ) {
							System.out.println( "  check OK" );
						}
						break;
					}
				}
			}
		}
		catch( final RuntimeException ex ) {
			if( isDebug ) {
				final String errMsg = "チェックエラー。 "
							+ " checkKey=" + checkKey
							+ " " + ex.getMessage();			// 5.1.8.0 (2010/07/01) errMsg 修正
				System.out.println( errMsg );
				System.err.println( ThrowUtil.ogStackTrace( ex ) );				// 6.4.2.0 (2016/01/29)
			}
			rtn = false;
		}
		return rtn;
	}

	/**
	 * 内部状態を文字列で返します。
	 *
	 * @return	このクラスの文字列表示
	 * @og.rtnNotNull
	 */
	@Override	// Object
	public String toString() {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "UrlCheckFilter" )
			.append( "filename=[" ).append( filename  ).append( "],")
			.append( "isDecode=[" ).append( isDecode  ).append( ']');		// 6.0.2.5 (2014/10/31) char を append する。
		return buf.toString();
	}
}
