    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.hayabusa.filter;
    017
    018import java.io.IOException;
    019import java.io.PrintWriter;
    020
    021import jakarta.servlet.Filter;
    022import jakarta.servlet.FilterChain;
    023import jakarta.servlet.FilterConfig;
    024import jakarta.servlet.ServletException;
    025import jakarta.servlet.ServletRequest;
    026import jakarta.servlet.ServletResponse;
    027import jakarta.servlet.http.HttpServletRequest;
    028
    029import org.opengion.fukurou.security.HybsCryptography;
    030import org.opengion.fukurou.util.StringUtil;
    031import org.opengion.hayabusa.common.HybsSystem;
    032import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;              // 6.1.0.0 (2014/12/26) refactoring
    033import org.opengion.fukurou.system.ThrowUtil;                                                   // 6.4.2.0 (2016/01/29)
    034import org.opengion.fukurou.system.HybsConst;                                                   // 6.4.5.2 (2016/05/06)
    035
    036import org.opengion.fukurou.util.FileUtil;                                                              // 6.4.5.2 (2016/05/06)
    037
    038/**
    039 * URLCheckFilter は、Filter インターフェースを継承した URLチェッククラスです。
    040 * web.xml で filter 設定することにより、該当のリソースに対して、og:linkタグで、
    041 * useURLCheck="true"が指定されたリンクURL以外を拒否することができます。
    042 * また、og:linkタグを経由した場合でも、リンクの有効期限を設定することで、
    043 * リンクURLの漏洩に対しても、一定時間の経過を持って、アクセスを拒否することができます。
    044 * また、リンク時にユーザー情報も埋め込んでいますので(初期値は、ログインユーザー)、
    045 * リンクアドレスが他のユーザーに知られた場合でも、アクセスを拒否することができます。
    046 *
    047 * システムリソースの「URL_CHECK_CRYPT」で暗号復号化のキーを指定可能です。
    048 * 指定しない場合はデフォルトのキーが利用されます。
    049 * キーの形式はHybsCryptographyに従います。
    050 *
    051 * フィルターに対してweb.xml でパラメータを設定します。
    052 *   ・filename :停止時メッセージ表示ファイル名
    053 *   ・ignoreURL:暗号化されたURLのうち空白に置き換える接頭文字列を指定します。
    054 *              外部からアクセスしたURLがロードバランサで内部向けURLに変換されてチェックが動作しないような場合に
    055 *              利用します。https://wwwX.のように指定します。通常は設定しません。
    056 *   ・debug     :標準出力に状況を表示します(true/false) 5.10.18.0 (2019/11/29)
    057 *   ・ommitURL :正規表現で、URLチェックを行わないパターンを記載します 5.10.18.0 (2019/11/29)
    058 *   ・ommitReferer:ドメイン(ホスト名)を指定すると、このRefererを持つものはURLチェックを行いません。 5.10.18.0 (2019/11/29)
    059 *              内部の遷移はチェックを行わず、外部からのリンクのみチェックを行う場合に利用します。
    060 *   ・ignoreRelative:trueにすると暗号化されたURLのうち、相対パスの箇所(../)を削除して評価します。  5.10.18.0 (2019/11/29)
    061 *
    062 * 【WEB-INF/web.xml】
    063 *     &lt;filter&gt;
    064 *         &lt;filter-name&gt;URLCheckFilter&lt;/filter-name&gt;
    065 *         &lt;filter-class&gt;org.opengion.hayabusa.filter.URLCheckFilter&lt;/filter-class&gt;
    066 *         &lt;init-param&gt;
    067 *             &lt;param-name&gt;filename&lt;/param-name&gt;
    068 *             &lt;param-value&gt;jsp/custom/refuseAccess.html&lt;/param-value&gt;
    069 *         &lt;/init-param&gt;
    070 *     &lt;/filter&gt;
    071 *
    072 *     &lt;filter-mapping&gt;
    073 *         &lt;filter-name&gt;URLCheckFilter&lt;/filter-name&gt;
    074 *         &lt;url-pattern&gt;/jsp/*&lt;/url-pattern&gt;
    075 *     &lt;/filter-mapping&gt;
    076 *
    077 * @og.group フィルター処理
    078 *
    079 * @version  4.0
    080 * @author   Hiroki Nakamura
    081 * @since    JDK5.0,
    082 */
    083public final class URLCheckFilter implements Filter {
    084
    085        private static final HybsCryptography HYBS_CRYPTOGRAPHY
    086                                                = new HybsCryptography( HybsSystem.sys( "URL_CHECK_CRYPT" ) ); // 5.8.8.0 (2015/06/05)
    087
    088        private static final String  USERID_HEADER = HybsSystem.sys( "USERID_HEADER_NAME" ); // 5.10.18.0 (2019/11/29)
    089
    090        private String  filename        = "jsp/custom/refuseAccess.html" ;                      // 6.3.8.3 (2015/10/03) アクセス拒否時メッセージ表示ファイル名
    091        private boolean isDebug         ;
    092        private boolean isDecode        = true;                 // 5.4.5.0(2012/02/28) URIDecodeするかどうか
    093
    094        private String  ignoreURL       ;                               // 5.8.6.1 (2015/04/17) 飛んできたcheckURLから取り除くURL文字列
    095        private String  ignoreRelative = "false";       // 5.10.18.1 (2019/12/09) 相対パスの../を無視する
    096        private String  ommitURL        ;                               // 5.10.11.0 (2019/05/03) URLチェックを行わないURLの正規表現
    097        private String  ommitReferer;                           // 5.10.11.0 (2019/05/03) URLチェックを行わないドメイン
    098
    099        private String encoding = "utf-8";      // 5.10.12.4 (2019/06/21) 日本語対応
    100
    101        /**
    102         * デフォルトコンストラクター
    103         *
    104         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
    105         */
    106        public URLCheckFilter() { super(); }            // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
    107
    108        /**
    109         * フィルター処理本体のメソッドです。
    110         *
    111         * @og.rev 6.2.0.0 (2015/02/27) new BufferedReader … を、FileUtil.getBufferedReader … に変更。
    112         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
    113         * @og.rev 6.3.8.3 (2015/10/03) アクセス拒否を示すメッセージファイルの内容を取り出します。
    114         * @og.rev 5.10.12.4 (2019/06/21) 日本語対応(encoding指定)
    115         * @og.rev 5.10.16.1 (2019/10/11) デバッグ追加
    116         *
    117         * @param       request         ServletRequestオブジェクト
    118         * @param       response        ServletResponseオブジェクト
    119         * @param       chain           FilterChainオブジェクト
    120         * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
    121         */
    122        @Override       // Filter
    123        public void doFilter( final ServletRequest request,
    124                                                        final ServletResponse response,
    125                                                        final FilterChain chain ) throws IOException, ServletException {
    126
    127                if( !isValidAccess( request ) ) {
    128                        if( isDebug ) {
    129                                System.out.println( "  check NG... " ); // 5.10.16.1
    130                        }
    131
    132                        response.setContentType( "text/html; charset=UTF-8" );
    133                        final PrintWriter out = response.getWriter();
    134                        out.println( refuseMsg() );                                                     // 6.3.8.3 (2015/10/03)
    135                        out.flush();
    136                        return;
    137                }
    138
    139                request.setAttribute( "RequestEncoding", encoding );    // 5.10.12.1 (2019/06/21) リクエスト変数で送信しておく
    140
    141                chain.doFilter(request, response);
    142        }
    143
    144        /**
    145         * フィルターの初期処理メソッドです。
    146         *
    147         * フィルターに対してweb.xml で初期パラメータを設定します。
    148         *   ・filename  :停止時メッセージ表示ファイル名(初期:jsp/custom/refuseAccess.html)
    149         *   ・decode    :URLデコードを行ってチェックするか(初期:true)
    150         *   ・ignoreURL :暗号化されたURLのうち空白に置き換える接頭文字列を指定します(初期:null)
    151         *   ・debug     :URLデコードを行ってチェックするか(初期:false)
    152         *
    153         * @og.rev 5.4.5.0 (2102/02/28)
    154         * @og.rev 5.7.3.2 (2014/02/28) Tomcat8 対応。getRealPath( "/" ) の互換性のための修正。
    155         * @og.rev 5.8.6.1 (2015/04/17) DMZのURL変換対応
    156         * @og.rev 6.2.4.1 (2015/05/22) REAL_PATH 対応。realPath は、HybsSystem経由で、取得する。
    157         * @og.rev 6.3.8.3 (2015/10/03) filenameの初期値設定。
    158         * @og.rev 5.10.11.0 (2019/05/03) ommitURL,ommitReferer
    159         * @og.rev 5.10.12.4 (2019/06/21) encoding
    160         * @og.rev 5.10.18.1 (2019/12/09) 相対パス対応
    161         *
    162         * @param filterConfig FilterConfigオブジェクト
    163         */
    164        @Override       // Filter
    165        public void init(final FilterConfig filterConfig) {
    166
    167                filename  = HybsSystem.getRealPath() + StringUtil.nval( filterConfig.getInitParameter("filename") , filename ); // 6.3.8.3 (2015/10/03)
    168                isDecode  = StringUtil.nval( filterConfig.getInitParameter("decode"), true );   // 5.4.5.0(2012/02/28)
    169                ignoreURL = filterConfig.getInitParameter("ignoreURL");                                                 // 5.8.6.1 (2015/04/17)
    170                ignoreRelative = filterConfig.getInitParameter("ignoreRelative");                               // 5.10.18.1 (2019/12/09)
    171                isDebug   = StringUtil.nval( filterConfig.getInitParameter("debug"), false );
    172
    173                ommitURL     = filterConfig.getInitParameter("ommitURL");                                               // 5.10.11.0 (2019/05/03)
    174                ommitReferer = filterConfig.getInitParameter("ommitReferer");                                   // 5.10.11.0 (2019/05/03)
    175
    176                encoding = StringUtil.nval( filterConfig.getInitParameter("encoding"), encoding ); // 5.10.12.4 (2019/06/21)
    177        }
    178
    179        /**
    180         * フィルターの終了処理メソッドです。
    181         *
    182         */
    183        @Override       // Filter
    184        public void destroy() {
    185                // ここでは処理を行いません。
    186        }
    187
    188        /**
    189         * アクセス拒否を示すメッセージ内容。
    190         *
    191         * @og.rev 6.3.8.3 (2015/10/03) アクセス拒否を示すメッセージファイルの内容を取り出します。
    192         * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
    193         * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
    194         *
    195         * @return アクセス拒否を示すメッセージファイルの内容
    196         */
    197        private String refuseMsg() {
    198                // アクセス拒否を示すメッセージファイルの内容を管理する FileString オブジェクトを構築する。
    199
    200                // 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
    201                return FileUtil.getValue( filename , HybsConst.UTF_8 );                 // 6.4.5.2 (2016/05/06)
    202        }
    203
    204        /**
    205         * フィルターの内部状態をチェックするメソッドです。
    206         *
    207         * @og.rev 5.4.5.0 (2012/02/28) Decode
    208         * @og.rev 5.8.6.1 (2015/04/17) DMZのURL変換対応
    209         * @og.rev 5.8.8.2 (2015/07/17) マルチバイト対応追加
    210         * @og.rev 6.3.8.4 (2015/10/09) デバッグメッセージの追加
    211         * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
    212         * @og.rev 5.10.11.0 (2019/05/03) ommitURL,ommitReferer
    213         * @or.rev 5.10.16 (2019/10/11) デバッグ追加
    214         * @og.rev 5.10.18.0 (2019/11/29) ヘッダ認証
    215         * @og.rev 5.10.18.1 (2019/12/09) 相対パス対応
    216         *
    217         * @param request ServletRequestオブジェクト
    218         *
    219         * @return      (true:許可  false:拒否)
    220         */
    221        private boolean isValidAccess( final ServletRequest request ) {
    222                // 6.3.8.4 (2015/10/09) デバッグメッセージの追加
    223                if( isDebug ) {
    224                        System.out.println( ((HttpServletRequest)request).getRequestURI() );
    225                }
    226
    227                // 5.10.11.0 (2019/05/03) データ取得位置変更
    228                String reqStr = ((HttpServletRequest)request).getRequestURL().toString();
    229
    230                // 5.10.11.0 referer判定追加
    231                // 入っている場合はtrueにする。
    232                final String referer  = ((HttpServletRequest)request).getHeader("REFERER");
    233                if(referer != null && ommitReferer != null && referer.indexOf( ommitReferer ) >= 0 ) {
    234                        if( isDebug ) {
    235                                System.out.println("URLCheck ommitRef"+reqStr);
    236                        }
    237                        return true;
    238                }
    239
    240                // リクエスト変数をURLに追加
    241                final String queryStr = ((HttpServletRequest)request).getQueryString();
    242                reqStr = reqStr + (queryStr != null ? "?" + queryStr : "");
    243
    244                // 5.10.11.0 ommitURL追加
    245                // ommitに合致する場合はtrueにする。
    246                if(ommitURL != null && reqStr.matches( ommitURL )) {
    247                        if( isDebug ) {
    248                                System.out.println("URLCheck ommitURL"+reqStr);
    249                        }
    250                        return true;
    251                }
    252
    253                String checkKey = request.getParameter( HybsSystem.URL_CHECK_KEY );
    254                if( checkKey == null || checkKey.isEmpty() ) {
    255                        if( isDebug ) {
    256//                              System.out.println( "  check NG [ No Check Key ]" );
    257                                System.out.println( "  check NG [ No Check Key ] = " + reqStr ); // 5.10.16.1 (2019/10/11) reqStr追加
    258                        }
    259                        return false;
    260                }
    261
    262                boolean rtn = false;
    263                try {
    264                        checkKey = HYBS_CRYPTOGRAPHY.decrypt( checkKey ).replace( "&amp;", "&" );
    265
    266                        if( isDebug ) {
    267                                System.out.println( "  checkKey=" + checkKey );
    268                        }
    269
    270                        // 5.8.6.1 (2015/04/17) DMZのURL変換対応 (ちょっと整理しておきます)
    271
    272                        final int tmAd = checkKey.lastIndexOf( ",time=" );
    273                        final int usAd = checkKey.lastIndexOf( ",userid=" );
    274
    275                        String url = checkKey.substring( 0 , tmAd );
    276                        final long  time    = Long.parseLong( checkKey.substring( tmAd + 6, usAd ) );
    277                        final String userid = checkKey.substring( usAd + 8 );
    278
    279                        // 4.3.8.0 (2009/08/01)
    280                        final String[] userArr = StringUtil.csv2Array( userid );
    281
    282                        // 5.8.6.1 (2015/04/17)ignoreURL対応
    283                        if( ignoreURL != null && ignoreURL.length()>0 && url.indexOf( ignoreURL ) == 0 ){
    284                                url = url.substring( ignoreURL.length() );
    285                        }
    286
    287                        // 5.10.18.1 (2019/12/09) ../を削除
    288                        if ( "true".equals( ignoreRelative ) ) {
    289                                url = url.replaceAll( "\\.\\./", "" );
    290                        }
    291
    292                        if( isDebug ) {
    293                                System.out.println( "   [ignoreURL]=" + ignoreURL );    // 2015/04/17 (2015/04/17)
    294                                System.out.println( "   [ignoreRelative]=" + ignoreRelative );
    295                                System.out.println( "   [url]      =" + url );
    296                                System.out.println( "   [vtime]    =" + time );
    297                                System.out.println( "   [userid]   =" + userid );
    298                        }
    299
    300//                      String reqStr =  ((HttpServletRequest)request).getRequestURL().toString() + "?" + ((HttpServletRequest)request).getQueryString();
    301                        // 5.4.5.0 (2012/02/28) URLDecodeを行う
    302                        if( isDecode ){
    303                                if( isDebug ) {
    304                                        System.out.println( "[BeforeURIDecode]="+reqStr );
    305                                }
    306                                reqStr = StringUtil.urlDecode( reqStr );
    307                                url = StringUtil.urlDecode( url );              // 5.8.8.2 (2015/07/17)
    308                        }
    309                        reqStr = reqStr.substring( 0, reqStr.lastIndexOf( HybsSystem.URL_CHECK_KEY ) -1 );
    310                        //      String reqStr =  ((HttpServletRequest)request).getRequestURL().toString();
    311//                      final String reqUser = ((HttpServletRequest)request).getRemoteUser();
    312                        String reqUser = ((HttpServletRequest)request).getRemoteUser() ; // 5.10.18.0 (2019/11/29)
    313                        if( USERID_HEADER != null && USERID_HEADER.length() > 0 && ( reqUser == null || reqUser.length() == 0) ) {
    314                                reqUser = ((HttpServletRequest)request).getHeader( USERID_HEADER );
    315                        }
    316
    317                        if( isDebug ) {
    318                                System.out.println( "   [reqURL] =" + reqStr );
    319                                System.out.println( "   [ctime]  =" + System.currentTimeMillis() );
    320                                System.out.println( "   [reqUser]=" + reqUser );
    321                                System.out.println( " endWith=" + reqStr.endsWith( url ) );
    322                                System.out.println( " times=" + (System.currentTimeMillis() - time) );
    323                                System.out.println( " [userArr.length]=" + userArr.length );
    324                        }
    325
    326                        if( reqStr.endsWith( url )
    327                                        && System.currentTimeMillis() - time < 0
    328                                        && userArr != null && userArr.length > 0 ) {
    329                                // 4.3.8.0 (2009/08/01)
    330                                for( int i=0; i<userArr.length; i++ ) {
    331                                        if( isDebug ) {
    332                                                System.out.println( " [userArr] =" + userArr[i] ); // 5.10.16.1
    333                                        }
    334                                        if( "*".equals( userArr[i] ) || reqUser.equals( userArr[i] ) ) {
    335                                                rtn = true;
    336                                                if( isDebug ) {
    337                                                        System.out.println( "  check OK" );
    338                                                }
    339                                                break;
    340                                        }
    341                                }
    342                        }
    343                }
    344                catch( final RuntimeException ex ) {
    345                        if( isDebug ) {
    346                                final String errMsg = "チェックエラー。 "
    347                                                        + " checkKey=" + checkKey
    348                                                        + " " + ex.getMessage();                        // 5.1.8.0 (2010/07/01) errMsg 修正
    349                                System.out.println( errMsg );
    350                                System.err.println( ThrowUtil.ogStackTrace( ex ) );                             // 6.4.2.0 (2016/01/29)
    351                        }
    352                        rtn = false;
    353                }
    354                return rtn;
    355        }
    356
    357        /**
    358         * 内部状態を文字列で返します。
    359         *
    360         * @return      このクラスの文字列表示
    361         * @og.rtnNotNull
    362         */
    363        @Override       // Object
    364        public String toString() {
    365                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
    366                        .append( "UrlCheckFilter" )
    367                        .append( "filename=[" ).append( filename  ).append( "],")
    368                        .append( "isDecode=[" ).append( isDecode  ).append( ']');               // 6.0.2.5 (2014/10/31) char を append する。
    369                return buf.toString();
    370        }
    371}