/*********************************************************************
* All Rights reserved,Copyright (c) K-Opticom
**********************************************************************
*＜プログラム内容＞
*   システム名      ：eo顧客基幹システム
*   モジュール名    ：JFUKyokaIpAdActionChkUtil
*   ソースファイル名：JFUKyokaIpAdActionChkUtil.java
*   作成者          ：富士通
*   日付            ：2012年04月04日
*＜機能概要＞
*   許可アクションリストファイルと許可IPアドレスファイルによるアクセス制限チェック部品です
*＜修正履歴＞
*   バージョン  修正日       修正者      修正内容
*   v3.00       2012/04/04   FJ）西川    新規作成 【ANK-0235-00-00】eoモバイルWiMAX受付対応(W10055)
*
**********************************************************************/
package eo.web.webview.common;

import static com.fujitsu.futurity.web.x31.X31SWebLog.DEBUG_LOG;

import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import eo.common.constant.JFUStrConst;

/**
 * 許可アクションリストファイルと許可IPアドレスファイルによるアクセス制限チェックを行う部品です。
 * <br>
 * @author 富士通
 */
public class JFUKyokaIpAdActionChkUtil
{
	/** IPアドレス形式文字列 */
	private static final String CHK_IP_ADDRESS = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$";

	/** IPアドレス形式文字列（ipv6） */
	private static final String CHK_IP_ADDRESS_IPV6 = "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}" +
														"(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)" +
														"(.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}" +
														"(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)" +
														"(.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}" +
														"(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)" +
														"(.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}" +
														"(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:" +
														"((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))" +
														"|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:" +
														"((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))" +
														"|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:" +
														"((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))" +
														"|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:" +
														"((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}" +
														"))|:)))(%.+)?\\s*$";

	/** エンコーディング(Shift_JIS) */
	private static final String ENCODE_SHIFT_JIS = "Shift_JIS";

	/** 半角スペース(char) */
	private static final char HALF_SPACE_CHAR = ' ';

	/** 全角スペース(char) */
	private static final char SPACE_CHAR = '\u3000';

	/** コロン(char) */
	private static final char COLON_CHAR = ':';

	/** コロン(String) */
	private static final String COLON_STR = ":";
	
	/** 空文字 */
	private static final String KARA_STR = "";

	/** 128ビット16進数文字列形式変換フォーマット */
	private static final String DECOMPRESS_IPV6_FORMAT = "%02x";

	/** デコード文字シーケンス  */
	private static final String DECODE_HEXDIGITS_STR = "0x";

	/** 正規表現:半角数字  */
	private static final String HANKAKU_NUM_REGEX = "\\d*";
	
	/** 正規表現:半角英大小文字、半角数字  */
	private static final String HANKAKU_EI_NUM_REGEX = "[a-zA-Z0-9]*";
	
	
	/**
	 * 許可アクションリストファイルと許可IPアドレスファイルによるアクセス制限チェックを行います。
	 * <br>
	 * @param ipad		IPアドレス
	 * @param action	アクション
	 * @return	true:アクセス許可／false:アクセス制限
	 * @throws Exception	プロパティファイルの読み込み時に発生する例外
	 */
	@SuppressWarnings("unchecked")
	public static boolean isAcskkIpAction(String ipad, String action) throws Exception
	{
		DEBUG_LOG.debug("----- JFUKyokaIpAdActionChkUtil#isAcskkIpAction [start] -----");
		DEBUG_LOG.debug("【IPアドレス】" + ipad);
		DEBUG_LOG.debug("【アクション】" + action);
		
		// ----------------------------------------------------
		// 定義情報の取得
		// ----------------------------------------------------
		HashMap<String, String> kyokaIpAddressMap = JFUKyokaIpAddressCache.getConstMap();	// 許可IPアドレス定義
		HashMap<String, String> kyokaIpActionMap = JFUKyokaActionCache.getConstMap();		// 許可アクション定義

		DEBUG_LOG.debug("【許可IPアドレス定義】" + kyokaIpAddressMap);
		DEBUG_LOG.debug("【許可アクション定義】" + kyokaIpActionMap);	
		
		// ----------------------------------------------------
		// 許可アクションチェック
		// ----------------------------------------------------
		// 許可アクション定義に含まれていない場合、アクセス制限
		if ((0 < JFUWebCommon.getLength(action)) && (!kyokaIpActionMap.containsKey(action)))
		{
			DEBUG_LOG.debug("アクションが許可アクションリストに存在しません");
			DEBUG_LOG.debug("----- JFUKyokaIpAdActionChkUtil#isAcskkIpAction [end] Result_NG -----");
			return false;
		}

		// ----------------------------------------------------
		// 許可IPアドレスチェック
		// ----------------------------------------------------
		boolean isKyokaIpAd = false;
		
		// 引数のIPアドレスがIPアドレスの形式でない場合、エラー
		if (!isIpAddress(ipad))
		{
			DEBUG_LOG.debug("IPアドレスの形式不正です");
			DEBUG_LOG.debug("----- JFUKyokaIpAdActionChkUtil#isAcskkIpAction [end] Result_NG -----");
			return false;
		}
		
		for (Entry<String, String> entry : kyokaIpAddressMap.entrySet())
		{
			// 定義情報マップ<Key:ネットワークアドレス, Value:サブネットマスク> ※値はtrim済
			String nwAddress = entry.getKey();		// ネットワークアドレス
			String subNetmask = entry.getValue();	// サブネットマスク>

			// 定義情報がIPアドレスの形式である場合のみ処理をおこなう
			if(isIpAddress(nwAddress) && isIpAddress(subNetmask))
			{
				// IPアドレスが指定されたサブネット内にいるかどうかを判定
				boolean ipCheckResult = isInSubnet(ipad, nwAddress, subNetmask) || isInSubnetForIpv6(ipad, nwAddress, subNetmask);

				// 定義に含まれる場合、チェックOKとしてLOOPを抜ける
				if (ipCheckResult)
				{
					isKyokaIpAd = true;
					break;
				}
			}
		}
		
		if (!isKyokaIpAd)
		{
			DEBUG_LOG.debug("IPアドレスが定義されているネットワークアドレス、サブネットアドレス内に存在しません");
			DEBUG_LOG.debug("----- JFUKyokaIpAdActionChkUtil#isAcskkIpAction [end] Result_NG -----");
		}
		else
		{
			DEBUG_LOG.debug("----- JFUKyokaIpAdActionChkUtil#isAcskkIpAction [end] Result_OK -----");
		}
		
		return isKyokaIpAd;
	}

	/**
	 * 渡された文字列がIPアドレスの形式であるかどうかをチェックします。<br>
	 * 
	 * @param ipAddress （ipv6対応）
	 *
	 * @return IPアドレスの形式であればtrue、IPアドレスの形式でなければfalseを返却.
	 */
	private static boolean isIpAddress(String ipAddress)
	{

		return ipAddress.matches(CHK_IP_ADDRESS) || ipAddress.matches(CHK_IP_ADDRESS_IPV6);
	}

	/**
	 * IPアドレスが指定されたサブネット内にいるかどうかを判定します。<br>
	 *
	 * @param ipAddress IPアドレス 例) 192.168.0.100
	 * @param networkAddress ネットワークアドレス 例) 192.168.0.0
	 * @param subnetMask サブネットマスク 例) 255.255.255.0
	 *
	 * @return サブネット内であればtrue、サブネット内でなければfalseを返却.
	 */
	private static boolean isInSubnet(String ipAddress, String networkAddress, String subnetMask)
	{

		try
		{
			long ipAddresslong = ipAddress2Long(ipAddress);
			long networkAddresslong = ipAddress2Long(networkAddress);
			long subnetMasklong = ipAddress2Long(subnetMask);
			long netAddresslong = ipAddresslong & subnetMasklong;

			// 指定されたネットワークアドレスと算出されたネットワークアドレスが一致すればtrue
			if (networkAddresslong != 0 && networkAddresslong == netAddresslong)
			{
				return true;
			}

		}
		catch (NumberFormatException ne)
		{
			// (ipv4形式用メソッドのため、ipv6形式のアドレスがプロパティに定義されていた場合、必ず発生する)
			DEBUG_LOG.debug("----- isInSubnet ----- NumberFormatException発生(想定内の例外のためスルーします)");
		}

		return false;
	}

	/**
	 * IPアドレスが指定されたサブネット内にいるかどうかを判定します。(ipv6対応)<br>
	 *
	 * @param ipAddress IPアドレス 例) 192.168.0.100、fe80:0:0:0:0204:61ff:254.157.241.86
	 * @param networkAddress ネットワークアドレス 例) 192.168.0.0、fe80:0:0:0::
	 * @param subnetMask サブネットマスク 例) 255.255.255.0、FFFF:FFFF:FFFF:FFFF::
	 *
	 * @return サブネット内であればtrue、サブネット内でなければfalseを返却.
	 */
	private static boolean isInSubnetForIpv6(String ipAddress, String networkAddress, String subnetMask)
	{

		try
		{
			BigInteger ipAddressBigInteger = ipAddress2BigInteger(ipAddress);
			BigInteger networkAddressBigInteger = ipAddress2BigInteger(networkAddress);
			BigInteger subnetMaskBigInteger = ipAddress2BigInteger(subnetMask);
			BigInteger netAddressBigInteger = ipAddressBigInteger.and(subnetMaskBigInteger);

			// 指定されたネットワークアドレスと算出されたネットワークアドレスが一致すればtrue
			if (!networkAddressBigInteger.equals(BigInteger.ZERO) && networkAddressBigInteger.equals(netAddressBigInteger))
			{
				return true;
			}

		}
		catch (NumberFormatException ne)
		{
			DEBUG_LOG.debug("----- isInSubnetForIpv6 ----- NumberFormatException発生(想定内の例外のためスルーします)");
		}

		return false;
	}

	/**
	 * 渡されたipv6の短縮形式文字列を128ビット16進数文字列形式に変換します。<br>
	 * 
	 * @param address （ipv6の短縮形式文字列）
	 *
	 * @return 128ビット16進数文字列形式のIPアドレス文字列.
	 * @throws UnknownHostException 
	 */
	private static String decompressIpv6(String address) throws UnknownHostException
	{
		InetAddress ipAddress = InetAddress.getByName(address);
		byte[] bs = ipAddress.getAddress();
		StringBuilder builder = new StringBuilder();
		for (int index = 0; index < bs.length; index++)
		{
			if (index > 0 && index % 2 == 0)
			{
				builder.append(COLON_CHAR);
			}
			builder.append(String.format(DECOMPRESS_IPV6_FORMAT, bs[index]));
		}
		return builder.toString();
	}

	/**
	 * IPアドレスを2進数のビット演算可能なlong型にします。 <br>
	 *  例)192.168.0.1 ⇒ 3232235521 <br>
	 *
	 * @param ipAddress IPアドレス
	 * @return IPアドレスをlong型にかえたもの
	 */
	private static long ipAddress2Long(String ipAddress)
	{
		return binStr2Long(ipAddress2BinStr(ipAddress));
	}

	/**
	 * IPアドレスを2進数のビット演算可能なBigInteger型にします。 <br>
	 *  例)fe80::204:61ff:fe9d:f156 ⇒ 338288524927261089654164245681446711638 <br>
	 *
	 * @param ipAddress IPアドレス
	 * @return IPアドレスをBigInteger型にかえたもの
	 */
	private static BigInteger ipAddress2BigInteger(String ipAddress)
	{
		return binStr2BigInteger(ipAddress2BinStr(ipAddress));
	}

	/**
	 * IPアドレスを2進数の文字列に変換したものをビット演算可能なlong型に変換<br>
	 *  例)11000000101010000000000000000001 ⇒ 3232235521 <br>
	 *
	 * @param binStrAddress IPアドレスを2進数の文字列に変換したもの
	 *
	 * @return 引数をlong型にかえたもの
	 */
	private static long binStr2Long(String binStrAddress)
	{
		// もし数値以外の文字がまじっていたら
		if (!isNumber(binStrAddress))
		{
			return 0;
		}

		return Long.valueOf(binStrAddress, 2).longValue();
	}

	/**
	 * IPアドレスを2進数の文字列に変換したものをビット演算可能なBigInteger型に変換<br>
	 *  例)11111110100000000000000000000000000000000000000000000000000000000000001000000100011000011111111111111110100111011111000101010110
	 *   ⇒ 338288524927261089654164245681446711638 <br>
	 *
	 * @param binStrAddress IPアドレスを2進数の文字列に変換したもの
	 *
	 * @return 引数をBigInteger型にかえたもの
	 */
	private static BigInteger binStr2BigInteger(String binStrAddress)
	{
		// もし数値以外の文字がまじっていたら
		if (!isNumber(binStrAddress))
		{
			return BigInteger.ZERO;
		}

		return new BigInteger(binStrAddress, 2);
	}

	/**
	 * IPアドレスを2進数の文字列にします。 <br>
	 *  例)192.168.0.1 ⇒ 11000000101010000000000000000001 <br>
	 *  例)fe80::204:61ff:fe9d:f156
	 *  ⇒ 11111110100000000000000000000000000000000000000000000000000000000000001000000100011000011111111111111110100111011111000101010110 <br>
	 *
	 * @param ipAddress IPアドレス
	 *
	 * @return IPアドレスを2進数の文字列に変換したもの
	 */
	private static String ipAddress2BinStr(String ipAddress)
	{
		String[] strAddressArray = toArray(ipAddress, ".");
		StringBuffer strBinAddress = new StringBuffer();

		// IPアドレスのフォーマットが「xxx.xxx.xxx.xxx」でなければエラー
		if (strAddressArray.length == 4)
		{
			// IPアドレスを0や1のビット配列文字列にする
			for (int i = 0; i < strAddressArray.length; i++)
			{

				// もし数値以外の文字がまじっていたら
				if (!isNumber(strAddressArray[i]))
				{
					return null;
				}
				strBinAddress.append(fillLeft(Integer.toBinaryString(new Integer(strAddressArray[i]).intValue()), JFUStrConst.S_ZERO, 8));
			}

			return strBinAddress.toString();
		}

		try
		{
			// IPアドレスのフォーマットが「XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX」でなければエラー
			strAddressArray = toArray(decompressIpv6(ipAddress), COLON_STR);
			if (strAddressArray.length == 8)
			{
				// IPアドレスを0や1のビット配列文字列にする
				for (int i = 0; i < strAddressArray.length; i++)
				{

					// もし英数字以外の文字がまじっていたら
					if (!isAlphaNumber(strAddressArray[i]))
					{
						return null;
					}
					strBinAddress.append(fillLeft(Integer.toBinaryString(Integer.decode(DECODE_HEXDIGITS_STR + strAddressArray[i])),
													JFUStrConst.S_ZERO, 16));
				}

				return strBinAddress.toString();
			}
		}
		catch (UnknownHostException e)
		{
			DEBUG_LOG.debug("----- ipAddress2BinStr ----- UnknownHostException発生");
		}
		return null;
	}

	/**
	 * 半角数字精査（マイナスはNG）。 <br>
	 * null、空文字はOK。 <br>
	 *
	 * @param value 対象オブジェクト.
	 * @return 半角数字のみであればtrue。それ以外はfalseを返却。
	 */
	private static boolean isNumber(Object value)
	{
		if (isEmpty(value))
		{
			return true;
		}

		return value.toString().matches(HANKAKU_NUM_REGEX);
	}

	/**
	 * 半角英大小文字、半角数字精査。 <br>
	 * null、空文字はOK。 <br>
	 *
	 * @param value 対象オブジェクト.
	 * @return 半角英数字のみであればtrue。それ以外はfalseを返却。
	 */
	private static boolean isAlphaNumber(Object value)
	{
		if (isEmpty(value))
		{
			return true;
		}

		return value.toString().matches(HANKAKU_EI_NUM_REGEX);
	}

	/**
	 * 空、nullであるかどうかを精査します。 <br>
	 * 文字列の前後のスペース（全角、半角両方）を取り除いてからチェックを行う。 <br>
	 *
	 * @param value 対象オブジェクト.
	 *
	 * @return true 空、nullである。 false 空でない。
	 */
	private static boolean isEmpty(Object value)
	{

		return value == null || trim(value).equals(KARA_STR);

	}

	/**
	 * 引数valueの前後のスペース（全角、半角両方）を除去します。
	 *
	 * @param value 対象オブジェクト.
	 *
	 * @return 変換後の文字列.
	 */
	private static String trim(Object value)
	{

		if (value == null)
		{
			return KARA_STR;
		}

		String buf = null;

		if (value instanceof String)
		{
			buf = (String)value;
		}
		else
		{
			buf = value.toString();
		}

		int size = buf.length();
		int offset = size - 1;
		int pos = 0;

		for (; offset >= 0; --offset)
		{
			char c = buf.charAt(offset);
			if (!(c == HALF_SPACE_CHAR || c == SPACE_CHAR))
			{
				break;
			}
		}

		if (offset > 0)
		{
			for (; pos < size; ++pos)
			{
				char c = buf.charAt(pos);
				if (!(c == HALF_SPACE_CHAR || c == SPACE_CHAR))
				{
					break;
				}
			}
		}

		return buf.substring(pos, offset + 1);

	}

	/**
	 * 文字列を区切り文字で配列に分解して返却します。
	 *
	 * @param value 分解対象の文字列
	 * @param delim 区切り文字
	 * @return 分解された文字列の配列
	 */
	private static String[] toArray(String value, String delim)
	{

		StringTokenizer st = new StringTokenizer(value, delim);

		String[] array = new String[st.countTokens()];
		int i = 0;
		while (st.hasMoreTokens())
		{
			array[i] = st.nextToken();
			i++;
		}
		return array;

	}

	/**
	 * 引数valueが引数lenの長さ(バイト数)に満たない場合、引数addで左側に残りの長さを埋める。 <br>
	 * SJISで処理を行う。
	 *
	 * @param value 対象オブジェクト.
	 * @param add 付加する文字列.
	 * @param max 最大文字数(バイト数).
	 *
	 * @return 作成後の文字列.
	 */
	private static String fillLeft(Object value, Object add, int max)
	{

		if (add == null)
		{
			return KARA_STR;
		}

		// null値は空文字へ変換
		String check = nullToStr(value, KARA_STR);
		int len = 0;
		try
		{
			len = getByteLength(check);
		}
		catch (Exception ex)
		{
			len = 0;
		}

		// 残りの長さまでを確認します
		if (len < max)
		{
			StringBuffer buf = new StringBuffer(max);
			for (int i = len; i < max; i++)
			{
				buf.append(add);
			}
			buf.append(check);

			return buf.toString();
		}
		else if (len > max)
		{
			return substrb(check, len - max, len);
		}

		return check;

	}

	/**
	 * getFileEncodingで得られるエンコーディングで解析した文字列の Byte数を返却します。 <br>
	 *
	 * @param value 解析する文字列.
	 *
	 * @return Byte数.
	 * @throws Exception すべての例外
	 */
	private static int getByteLength(String value) throws Exception
	{

		return value.getBytes(ENCODE_SHIFT_JIS).length;

	}

	/**
	 * 引数valueがnullの場合、引数optionで指定された値を返却します。
	 *
	 * @param value 対象のオブジェクト.
	 * @param option 引数valueがnullの場合返却する値.
	 *
	 * @return 比較後の値.
	 */
	private static String nullToStr(Object value, String option)
	{

		if (value == null)
		{
			return option;
		}
		else
		{
			return value.toString();
		}
	}

	/**
	 * バイト数で文字列を切り出す。
	 *
	 * @param value 対象のオブジェクト
	 * @param p1 オフセット
	 * @param p2 桁数
	 *
	 * @return 切り出し後の文字列。
	 */
	private static String substrb(Object value, int p1, int p2)
	{

		if (value == null)
		{
			return null;
		}

		String check = null;
		if (value instanceof String)
		{
			check = (String)value;
		}
		else
		{
			check = value.toString();
		}

		// バイトで切り取る
		try
		{
			check = new String(check.getBytes(ENCODE_SHIFT_JIS), p1, p2 - p1, ENCODE_SHIFT_JIS);
		}
		catch (Exception ex)
		{
			check = check.substring(p1, p2);
		}

		return check;

	}
}
