end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

jQuery.validation (javascript)の練習

やはり調べた範囲では、client側のvalidationは jQuery.validation が、よさそ

参考url

html & javascript source

詳細は、src 内のコメントを参照下さい。

src内に記載していない機能も多くありますので、 それらは、 https://jqueryvalidation.org/validate/ を参照下さい。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<script src="//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/jquery-validation@1.19.2/dist/jquery.validate.min.js"></script>
<!--〒→住所変換 by google 日本語入力api-->
<script src="/contact2/common/zip2address.js"></script>
<!--自動フリガナ入力-->
<script src="/contact2/common/jquery.autoKana.js"></script>


<script src="foo.js"></script>
</head>
<body class="catalogForm">
  <form action="" method="post" name="form1" id="form1" class="clearDiv">
    <div>
      <input type="text" name="name"      placeholder="氏名(必須)"/>
      <input type="text" name="name_kana" placeholder="氏名カナ"/>
    </div>
  
    <div>
      <input type="text" name="tel"       placeholder="TEL"/>
    </div>
  
    <div>
      <input type="text" name="mail"      placeholder="mail"/>
      <input type="text" name="mail2"     placeholder="mail確認"/>
    </div>
  
  <input type="text" name="zipcode"   placeholder="郵便番号"/>
  <button type="button" onClick="myform.conv_zip2address()"
          >〒→住所変換</button><br/>
  <input type="text" name="address_pref"  placeholder="都道府県"/>
  <input type="text" name="address_city"  placeholder="区市町村"/>
  <input type="text" name="address_other" placeholder="住所 その他"/><br/>

  <select name="job" style="width:175px;">
    <option value="">職業選択欄</option>
    <option value="会社員">会社員</option>
    <option value="公務員">公務員</option>
    <option value="学生"  >学生</option>
    <option value="その他">その他</option>
  </select>
  <input type="text" name="job_other"
     placeholder="職業=その他の場合、入力"/><br/>

  
  訪問のきっかけ(最大2個選択)<br/>
  <label style="display:inline-block; width:175px;">
    <input type="checkbox" name="visit_trriger" value="tv"/>テレビ
  </label>
  <label style="display:inline-block; width:175px;">
    <input type="checkbox" name="visit_trriger" value="homepage"/>ホームページ
  </label>
  <label style="display:inline-block; width:175px;">
    <input type="checkbox" name="visit_trriger" value="homepage"/>知人の紹介
  </label>
  <br/>

  <textarea name="opinion" style="width:500px; height:50px;"
            >よろしければ、ご意見をお聞かせ下さい(10文字以内)</textarea>
  <br/>
  <button type="button" onClick="myform.validate_all()">VALIDATE</button>
</form>

<script> $(document).ready( function(){ myform.init_input_page(); }); </script>
</body>
</html>
(function() {
    var MyForm = function() {};
    MyForm.prototype = {
        
        validate_rule: {
            "rules" :
            {"name"      :{"required" : true,
                           "rangelength": [1,50] },
             //「is_katakana」は独自作成のrule
             "name_kana" :{"required" : true, is_katakana:true},
             "mail"      :{email: true },
             "mail2"     :{email: true, equalTo: 'input[name="mail"]' },
             // 「職業=その他」を選択した場合、text欄に入力
             "job_other" :{
                 "required":{"depends":function(element) {
                     return true ? $('select[name="job"]').val()=="その他": false;
                 }}},
             // checkbox選択数のvalidation
             "visit_trriger":{"rangelength": [0, 2] }, //checkboxも選択数を指定可
             "opinion"      :{"rangelength": [0,10] }
            },
            messages:
            {"name"     :{"required" :"氏名は必須項目です",
                          "maxlength":"氏名は50文字以内で入力下さい"},
             "name_kana":{"required" :"氏名(カナ)は必須項目です",
                          "is_katakana": "氏名(カナ)はカタカナで入力下さい"},
             "mail"     :{"email"    :"@ のあるメールアドレス形式で入力下さい"},
             "mail2"    :{"email"    :"@ のあるメールアドレス形式で入力下さい",
                          "equalTo"  :"同じメールアドレスを2箇所へ入力下さい" },
             "job_other":{"required" :"職業(その他)の場合、入力下さい"},
             "visit_trriger":{"rangelength":"訪問のきっかけは2個まで選択可能です"},
             "opinion"  :{"maxlength":"10文字以内で入力下さい"}
            },
        },

        validate_opt_rule: {
            // validation後のtagやclass等
            errorElement: "span", // validation NG 時の要素が label->span
            errorClass:   "validate_NG",// validation NG 時のclass
            validClass:   "validate_OK",// validation OK 時のclass
            //validation実行のタイミング
            onsubmit  :false,
            onfocusout:false,
            onkeyup   :false,
            onclick   :false
        },
        
        init_input_page: function(){
            // jquery.validation の有効化
            $("#form1").validate(Object.assign(this.validate_rule,
                                               this.validate_opt_rule));
            // jquery.validation の独自ruleの追加
            this.add_opt_validate_method();

            $('input[type="text"]').attr("autocomplete","off");
            
            //氏名→氏名カナ の自動入力
            $.fn.autoKana('input[name="name"]',
                          'input[name="name_kana"]',
                          {katakana:true });
        },
        
        add_opt_validate_method:function(){
            var this_obj = this;
            // カタカナ validationの独自rule
            // 内部でカタカナ化の変換を行っていますが
            // 別method & 別タイミングで行った方がいいかも
            jQuery.validator.addMethod("is_katakana", function(value, element) {
                if(this.optional(element)) return true; //未入力はcheckしない

                value = this_obj.conv_zenkaku_ascii_to_han(value);
                value = this_obj.conv_zenkaku_hira_to_kana(value);
                value = this_obj.conv_hankaku_kana_to_zen(value);
                $(element).val(value);
                
                //「カナ」に加え、半角英数字記号もOKとしています
                re_pat = '^(['+'ァ-ヶー'+'a-zA-Z0-9!-/:-@¥[-`{-~'+']+)$';
                var re = new RegExp(re_pat,'');
                return re.test(value);
            });
        },

        validate_all: function(){
            alert("result of validate_all:"+ $('#form1').valid() );
        },

        // refer to https://github.com/hokaccha/js-zip2address
        conv_zip2address: function(){
            var zipcode = $('input[name="zipcode"]').val().trim();
            zipcode = this.conv_zenkaku_ascii_to_han(zipcode);
            $('input[name="zipcode"]').val(zipcode);
            
            zip2address(zipcode, function(address) {
                if (! address) {
                    return null;
                }

                // google日本語入力API が、区市町村+町 で返す為、parse
                var address_city = address.city.match(/^.+?[区市町村]/);
                var address_pref_city = address.pref+address_city;
                var address_other = address.all;
                address_other = address_other.replace(address_pref_city, '');
                
                $('input[name="address_pref"]').val(address.pref);
                $('input[name="address_city"]').val(address_city);
                $('input[name="address_other"]').val(address_other);
            });
        },

        // 全角英数字記号->半角
        conv_zenkaku_ascii_to_han: function(org_str){
            var half_str =
                org_str.replace(
                    /[!-~]/g,
                    function( tmp_str ) {
                        return String.fromCharCode(tmp_str.charCodeAt(0) - 0xFEE0);
                    }
                );
            // 上記のcode shiftで変換NG文字の変換
            return half_str.replace(/”/g, "\"")
                .replace(/’/g, "'")
                .replace(/‘/g, "`")
                .replace(/¥/g, "\\")
                .replace(/ /g, " ")
                .replace(/〜/g, "~");
            return half_str;
        },
        // 全角ひらがな->カナ
        conv_zenkaku_hira_to_kana: function(org_str){
            
            new_str = org_str.replace(/[\u3041-\u3096]/g, function(match) {
                var chr = match.charCodeAt(0) + 0x60;
                return String.fromCharCode(chr);
            });
            return new_str;
        },
        
        // 半角カナ->全角
        conv_hankaku_kana_to_zen: function(org_str){
            var kana_map ={
                // 濁音や半濁音は先に定義
                'ガ': 'ガ', 'ギ': 'ギ', 'グ': 'グ', 'ゲ': 'ゲ', 'ゴ': 'ゴ',
                'ザ': 'ザ', 'ジ': 'ジ', 'ズ': 'ズ', 'ゼ': 'ゼ', 'ゾ': 'ゾ',
                'ダ': 'ダ', 'ヂ': 'ヂ', 'ヅ': 'ヅ', 'デ': 'デ', 'ド': 'ド',
                'バ': 'バ', 'ビ': 'ビ', 'ブ': 'ブ', 'ベ': 'ベ', 'ボ': 'ボ',
                'パ': 'パ', 'ピ': 'ピ', 'プ': 'プ', 'ペ': 'ペ', 'ポ': 'ポ',
                'ヴ': 'ヴ', 'ヷ': 'ヷ', 'ヺ': 'ヺ',
                'ア': 'ア', 'イ': 'イ', 'ウ': 'ウ', 'エ': 'エ', 'オ': 'オ',
                'カ': 'カ', 'キ': 'キ', 'ク': 'ク', 'ケ': 'ケ', 'コ': 'コ',
                'サ': 'サ', 'シ': 'シ', 'ス': 'ス', 'セ': 'セ', 'ソ': 'ソ',
                'タ': 'タ', 'チ': 'チ', 'ツ': 'ツ', 'テ': 'テ', 'ト': 'ト',
                'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ',
                'ハ': 'ハ', 'ヒ': 'ヒ', 'フ': 'フ', 'ヘ': 'ヘ', 'ホ': 'ホ',
                'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ',
                'ヤ': 'ヤ', 'ユ': 'ユ', 'ヨ': 'ヨ',
                'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ',
                'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン',
                'ァ': 'ァ', 'ィ': 'ィ', 'ゥ': 'ゥ', 'ェ': 'ェ', 'ォ': 'ォ',
                'ッ': 'ッ', 'ャ': 'ャ', 'ュ': 'ュ', 'ョ': 'ョ',
                '。': '。', '、': '、', 'ー': 'ー', '「': '「', '」': '」', '・': '・'};

            var re = new RegExp('('+Object.keys(kana_map).join('|')+')','g');
            return org_str.replace(re, function (match) {
                return kana_map[match];
            }).replace(/゙/g, '゛').replace(/゚/g, '゜');
        }
    };

    window.myform = new MyForm();
    
})();

php で conf 形式のfile読込み - ただし、「:」セパレータ

perlでは、Config::Auto という moduleがあり、 セパレータとして「=」や「:」を指定できます。

https://metacpan.org/pod/Config::Auto

phpにも、parse_ini_file() がありますが、セパレータは「=」のみのようです。

https://www.php.net/manual/ja/function.parse-ini-file.php

なので、自前で「:」セパレータのparse_ini_file()を書くと、 次のようになります。

<?php

$ini_path = '/home/end0tknr/hogehoge/contact/config.ini';

main();

function main(){

    global $ini_path;
    $fh = fopen($ini_path, "r") or die("fail fopen $ini_path");
    
    $conf = array();
    while ( $line = fgets($fh) ) {
        $line = mb_ereg_replace(';.*', '', trim($line));
        if( mb_strlen($line)==0 ){
            continue;
        }

        $atri_key_val = explode(":",$line);
        $conf[trim($atri_key_val[0])] = trim($atri_key_val[1]);
    }
    fclose($fh);
    
    echo(var_dump($conf) ."\n");
    //        echo($line ."\n");
}

php (PDO)による DB接続~SQL実行

以下の通りです。

先日の 問合せフォーム用 ajax web api server side php code - end0tknr's kipple - 新web写経開発 に続く、手習い

<?php

$db_conf = array(
                 'dsn'   =>'mysql:dbname=xing;host=localhost;charset=utf8',
                 'user'  =>'root',
                 'pw'    =>'ないしょ',
                 'option'=>array(PDO::ATTR_AUTOCOMMIT=>true,
                                 PDO::ATTR_ERRMODE   =>PDO::ERRMODE_EXCEPTION) );

init();
main();


function main(){
    global $db_conf;
    $dbh = connect_db($db_conf);

    try {
        $sth = $dbh->prepare('SELECT * FROM test_tbl limit ?');
        //$sth->bindValue(1, "HGOE", PDO::PARAM_STR);
        $sth->bindValue(1, 10, PDO::PARAM_INT);
        $sth->execute();
    } catch (PDOException $e) {
        $tmp_msg = "fail sql ". $e->getMessage();
        error_log($tmp_msg);
        die($tmp_msg);
        // return null;
    }

    foreach ($sth as $row) {
        error_log(var_dump($row));
    }

    $pdo = null; // DB切断
}


function connect_db($db_conf){
    try {
        $dbh = new PDO($db_conf['dsn'],
                       $db_conf['user'],$db_conf['pw'],
                       $db_conf['option']);
    } catch (PDOException $e) {
        $tmp_msg = "fail connect_db()". $e->getMessage(); 
        error_log($tmp_msg);
        die($tmp_msg);
    }
    return $dbh;
}


function init(){  // php.ini 代替設定
    // ini_set('extension', 'php_pdo.dll');

    // error_log() method により、apache error_log へ出力する為
    ini_set('display_errors', 1);
    ini_set("error_reporting",E_ALL);
    ini_set("log_errors","on");
    ini_set('error_log', 'php://stderr');
    // ini_set('error_log', '/path/to/logs/php.log');
    
    ini_set("date.timezone", "Asia/Tokyo");

    // session有効期間
    ini_set( 'session.gc_divisor', 1);
    ini_set( 'session.gc_maxlifetime', 60*30 ); //sec
}

mysql 8.0.17 で varchar() の合計文字数が 65535超の場合、「ERROR 1118 (42000): Row size too large.」

以下の通り、エラーとなりました。varhcar -> text とすることで解消するようです

mysql> CREATE TABLE contact_form_template (
    ->  id            integer        primary key AUTO_INCREMENT,
    ->  uid           varchar(1024),
    ->  form          varchar(1024),
    ->  ext1          varchar(4096),
    ->  ext2          varchar(4096),
          :
    ->  ext85         varchar(4096)
    -> );
ERROR 1118 (42000): Row size too large.
The maximum row size for the used table type, not counting BLOBs, is 65535.
This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

OpenAMコンソーシアムのOpenAM14? 15? を srcから build

次のurlを参考/そのままに OpenAM14? 15? (※)を srcから buildします。

(※ OpenAM14を git clone https://github.com/openam-jp/openam したつもりが OpenAM15のようでした...)

warファイルやdockerファイルは、上記の OpenIdentityPlatform から入手可能です。

build環境

現在あるjava等のversionは以下の通り

$ cat /etc/redhat-release 
  CentOS Linux release 8.1.1911 (Core)
$/usr/bin/java -version
  openjdk version "1.8.0_252"
$ mvn -version
  Apache Maven 3.5.4 (Red Hat 3.5.4-5)

依存libraryのbuild

$ git clone https://github.com/openam-jp/forgerock-parent
$   cd forgerock-parent        ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-bom
$   cd forgerock-bom           ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-build-tools
$   cd forgerock-build-tools   ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-i18n-framework
$   cd forgerock-i18n-framework; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-guice
$   cd forgerock-guice         ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-ui  【※1】
$   cd forgerock-ui            ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-guava
$   cd forgerock-guava         ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-commons
$   cd forgerock-commons       ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-persistit
$   cd forgerock-persistit     ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/forgerock-bloomfilter
$   cd forgerock-bloomfilter   ; mvn clean install; cd ..
$ git clone https://github.com/openam-jp/opendj-sdk
$   cd opendj-sdk
$   mvn clean install -DskipTests 【※2】
$   cd ..
$ git clone https://github.com/openam-jp/opendj
$   cd opendj                  ; mvn clean install; cd ..

※1 forgerock-ui には、nodejs, npm がバンドルされていますが、 「mvn clean install」で「npm WARN deprecated circular-json@0.3.3」エラー表示後、 処理が完全に止まっているようでしたので、httpsが怪しいと考え 「$ npm config set registry http://registry.npmjs.org/」を実施しました。

[INFO] Running 'npm install --color=false' in /home/end0tknr/tmp/forgerock-ui-13.0.5/forgerock-ui-commons
[ERROR] npm WARN deprecated eslint-formatter-warning-summary@1.0.1: this package has been deprecated
[ERROR] WARN engine jsdoc@3.6.4: wanted: {"node":">=8.15.0"} (current: {"node":"4.2.6","npm":"3.5.3"})
[ERROR] npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
[ERROR] npm WARN deprecated circular-json@0.3.3: CircularJSON is in maintenance only, flatted is its successor.

※2 私の環境では「mvn clean install」のテストに失敗しました。 ただ、opendjでなく、openldap を今後、使用予定ですので、 「mvn clean install -DskipTests」で回避?しました。

python2.7に依存

OpenAM14?, 15?は、python2.7(≠3)に依存する為、今更?、python2.7をinstall

$ wget https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tgz
$ tar -xvf Python-2.7.18.tgz
$ cd Python-2.7.18
$ ./configure --prefix=/usr/local/python2 --enable-optimizations

$ vi Modules/Setup 【※3】
  SSL=/usr/local/openssl_1_1_1
  _ssl _ssl.c \
      -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \
      -L$(SSL)/lib -lssl -lcrypto

$ make
$ make test
$ sudo make install

$ export PYTHON=/usr/local/python2/bin/python

※3 opensslを /usr/local/openssl_1_1_1 へinstallしていた為、 Modules/Setupを編集

openam 本体のbuild

$ git clone https://github.com/openam-jp/openam
$ cd openam
$ mvn clean install

すると、以下にwarファイルが作成されます
$ ls -l openam-server/target/*war
-rw-rw-r--  1 end0tknr end0tknr 115636094 Jul  1 12:44 OpenAM-15.0.0-SNAPSHOT.war

vue.js における Invalid Host header を回避

vue.js 場合

vue.js の install

$ /usr/bin/node --version
v10.19.0
$ /usr/bin/npm --version
6.13.4

$ npm install @vue/cli
+ @vue/cli@4.4.6

$ npm install @vue/cli-service-global
+ @vue/cli-service-global@4.4.6

vue.js 用 テスト アプリ作成と起動

$ ./node_modules/@vue/cli/bin/vue.js create vue_test_app

$ cd vue_test_app
$ npm run serve (※1)

※1 または  ./node_modules/.bin/vue-cli-service help serve
            ./node_modules/.bin/vue-cli-service serve

この状態で、virtual boxのhost os側からブラウザアクセスすると、 ブラウザでアクセスをした際、"Invalid Host header" と表示される為、 vue.config.jsを作成

$ vi vue_test_app/vue.config.js
  module.exports = {
    devServer: {
      disableHostCheck: true
    }
  }

この状態で、改めて npm run serve により起動後、ブラウザでアクセスすると、 解消しています。

nuxt.js の場合

nuxt.js の install

$ mkdir nuxt_test_app
$ cd nuxt_test_app

$ vi package.json
{"name": "my-app",
 "scripts": { "dev": "nuxt" }
}

$ npm install --save nuxt
  + nuxt@2.13.2
$

nuxt.js の場合、何もしなくてもアクセスできます。

nuxt.js の場合、何もしなくてもアクセスできます。

$ npm run dev

もし、nuxt.config.js を作成し、hostやportを指定する場合、 「host: 'cent80.a5.jp'」のようにすると、接続できない為、 「host: '0.0.0.0'」と指定します。

$ vi nuxt.config.js
export default {
  server: {
      host: '0.0.0.0',
//    host: 'cent80.a5.jp',
      port: 8081
  }
}

問合せフォーム用 ajax web api server side php code

手習い。

<?php

init();
main();


function init(){  // php.ini 代替設定
    // error_log() method により、apache error_log へ出力する為
    ini_set('display_errors', 1);
    ini_set("error_reporting",E_ALL);
    ini_set("log_errors","on");
    ini_set('error_log', 'php://stderr');
    // ini_set('error_log', '/path/to/logs/php.log');
    
    ini_set("date.timezone", "Asia/Tokyo");

    // session有効期間
    ini_set( 'session.gc_divisor', 1);
    ini_set( 'session.gc_maxlifetime', 60*30 ); //sec
}

function main(){

    foreach($_GET as $get_param_key => $get_param_val){
        //入力画面→確認画面 遷移時
        if(strcmp($get_param_key,"callback_input_to_confirm")==0){
            return main_input_to_confirm($get_param_key);
        }
        //確認画面 表示時
        if(strcmp($get_param_key,"callback_init_confirm_page")==0){
            return main_init_confirm_page($get_param_key);
        }
        //確認画面→完了画面 遷移時
        if(strcmp($get_param_key,"callback_confirm_to_complete")==0){
            return main_confirm_to_complete($get_param_key);
        }
    }
}

//入力画面→確認画面 遷移時
function main_input_to_confirm($callback_method){

    // session id設定とsession開始
    session_id(sha1(uniqid(microtime())));
    if( session_start() ){
        $session_id = session_id();
    }
    // TODO フォームIDの考え方は、現行担当に要確認
    $form_id = "1";
    
    // フォーム定義(設定)のload
    $form_defs = load_form_defs($form_id);
    $params_defs = $form_defs["definition"];
    
    // request parameterの取得とvalidation
    $req_params_tmp = load_req_params($params_defs);
    $req_params = $req_params_tmp[0];
    $errors     = $req_params_tmp[1];

    $req_params_json = json_encode($req_params,JSON_UNESCAPED_UNICODE);
    $_SESSION['req_params'] = $req_params_json;

    $req_result = array();
    if( count($errors)==0 ){
        $req_result["result"]     = "OK";
        $req_result["session_id"] = $session_id;
        $req_result_json = json_encode($req_result, JSON_UNESCAPED_UNICODE);

        echo $callback_method ."(".$req_result_json .");";
        return;
    }

    $req_result["result"]     = "NG";
    $req_result["errors"]     = $errors;
    $req_result_json = json_encode($req_result, JSON_UNESCAPED_UNICODE);
    
    echo $callback_method ."(".$req_result_json .");";
    // セッション情報は気
    $_SESSION = array();
    session_destroy();
    return;
}

//確認画面 表示時
function main_init_confirm_page($callback_method){
    // session情報を入力画面から引継ぎ
    $session_id = $_POST["session_id"];
    session_id($session_id);
    session_start();

    $req_params_json = $_SESSION['req_params'];
    try{
        $req_params = json_decode($req_params_json,true,512,JSON_THROW_ON_ERROR);
    }catch(JsonException $e){
        $tmp_msg =
            implode(" ",["json_decode()",$e->getMessage(),$req_params_json]);
        error_log($tmp_msg);
        echo $callback_method .'({"result":"NG","sys_msg":"bad session or bad json"});';
        return;
    }

    $req_result = array();
    $req_result["result"]     = "OK";
    $req_result["req_params"] = $req_params;
    $req_result_json = json_encode($req_result, JSON_UNESCAPED_UNICODE);

    echo $callback_method ."(".$req_result_json .");";
}

//確認画面→完了画面 遷移時
function main_confirm_to_complete($callback_method){
    $session_id = $_POST["session_id"];
    session_id($session_id);
    session_start();

    $req_params_json = $_SESSION['req_params'];

    // TODO 送信処理として、DB登録やメール送信?があるが、
    //      既存のphp srcが、そのまま動作するらしい為、後日

    $req_result = array();
    $req_result["result"]     = "OK";
    $req_result_json = json_encode($req_result, JSON_UNESCAPED_UNICODE);

    echo $callback_method ."(".$req_result_json .");";

    $_SESSION = array();
    session_destroy();
}

function load_req_params($params_defs){
    $req_params = array();
    $errors     = array();
    
    foreach($params_defs as $atri_key => $param_defs){
        if(! array_key_exists($atri_key, $_POST)){
            //必須項目の場合、error情報を設定
            if(array_key_exists("requires", $param_defs) && $param_defs["requires"] ){
                if(! array_key_exists($atri_key, $errors)){
                    $errors[$atri_key] = [];
                }
                array_push($errors[$atri_key],$param_defs["e_require"]);
            }
            continue;
        }
        
        // 送信dataがtextの場合
        if(! is_array($_POST[$atri_key]) ){
            //validate
            $checked_info = validate_param($param_defs,$_POST[$atri_key]);
            $atri_val = $checked_info[0];
            $err_tmp  = $checked_info[1];
            
            if(mb_strlen($atri_val)){
                $req_params[$atri_key] = $atri_val;
            }
            // validataionによるerror情報がある場合
            if(count($err_tmp)){
                if(! array_key_exists($atri_key, $errors)){
                    $errors[$atri_key] = [];
                }
                array_push($errors[$atri_key],$err_tmp);
            }
            
            continue;
        }

        // 送信dataが配列の場合
        foreach($_POST[$atri_key] as $idx => $atri_val){
            //validate
            $checked_info = validate_param($param_defs,$atri_val);
            $atri_val = $checked_info[0];
            $err_tmp  = $checked_info[1];

            if(mb_strlen($atri_val)){
                if(! array_key_exists($atri_key, $req_params)){
                    $req_params[$atri_key] = [];
                }
                array_push($req_params[$atri_key],$atri_val);
            }
            // validataionによるerror情報がある場合
            if(count($err_tmp)){
                if(! array_key_exists($atri_key, $errors)){
                    $errors[$atri_key] = [];
                }
                array_push($errors[$atri_key],$err_tmp);
            }
        }
    }
    return array($req_params,$errors);
}


function validate_param($param_defs,$atri_val){
    $errors = array();

    $atri_val = my_trim($atri_val);
    
    //最大文字数
    if(array_key_exists("max", $param_defs)){
        if($param_defs["max"] < mb_strlen($atri_val)){
            array_push($errors,$param_defs["e_length"]);
        }
    }
    //その他rule
    if(array_key_exists("rule", $param_defs)){
        if(strcmp($param_defs["rule"],"ZENKAKU")==0){ //全角
            $atri_val = mb_convert_kana($atri_val, "AKV"); //半角->全角変換
            
        }elseif(strcmp($param_defs["rule"],"KKANA")==0){ //全角カナ
            $atri_val = mb_convert_kana($atri_val, "KC"); //半角->全角変換
            // 名前によっては、数字等を含む場合も考えられる為、厳密なカナチェックは行わない
            // array_push($errors,$param_defs["e_rule"]);

        }elseif(strcmp($param_defs["rule"],"EMAIL")==0 ||
                strcmp($param_defs["rule"],"POST" )==0 ||
                strcmp($param_defs["rule"],"TEL"  )==0 ){
            $atri_val = mb_convert_kana($atri_val, "as"); //全角->半角変換
            if(! preg_match("/^[[:graph:]|[:space:]]+$/i", $atri_val)) {
                array_push($errors,$param_defs["e_rule"]);
            }
        }
    }
    return array($atri_val,$errors);
}

function load_form_defs($form_id){
    $form_def_file = "etc/form_def_$form_id.json";
    $file_obj = new SplFileObject($form_def_file,"rb");
    $file_data = $file_obj->fread($file_obj->getSize());
    
    $form_defs = array();
    try{
        $form_defs = json_decode($file_data,true,512,JSON_THROW_ON_ERROR);
    }catch(JsonException $e){
        $tmp_msg =
            implode(" ",["json_decode()",$e->getMessage(),$form_def_file]);
        error_log($tmp_msg);
        return  array();
    }
    return $form_defs;
}

function my_trim($org_str){
    return trim(trim($org_str)," ");
}

python で apache access_log (gzip形式)をparse

apache access_log にあるuser agentからブラウザを判定 - end0tknr's kipple - 新web写経開発

apacheのログ(access_log)解析は、Apache::ParseLog 等のcpan moduleより正規表現 - end0tknr's kipple - 新web写経開発

perlで書いた上記エントリを、pythonで書いてみた。

正規表現と *.gz ファイルの読取りの練習です

#!/usr/local/python3/bin/python3
# -*- coding: utf-8 -*-
import gzip
import re
import sys
import datetime

# apache access_log用 正規表現
re_pat_log_line = \
    " ".join(['^([^ ]*) ([^ ]*) ([^ ]*) \[([^]]*)\] "([^ ]*)(?: *([^ ]*)',
              '*([^ ]*))?" ([^ ]*) ([^ ]*) "(.*?)" "(.*?)"'])
re_log_line = re.compile(re_pat_log_line)

# access_log 日時 用 正規表現 例:12/Jun/2020:04:27:27 +0900
re_pat_time = '^(\d+)/(\S+)/(\d+):(\d+):(\d+):(\d+)'
re_time = re.compile(re_pat_time)

# access_log 集計対象外 用
re_pat_ext = '.+\.(js|css|ico|gif|jpg|png)\??.*$'
re_ext = re.compile(re_pat_ext)

# month str->int
month_def = {"Jan":1,"Feb":2,"Mar":3,"Apr": 4,"May": 5,"Jun":6,
             "Jul":7,"Aug":8,"Sep":9,"Oct":10,"Nov":11,"Dec":12}


def main():
    access_log_gzs = sys.argv
    access_log_gzs.pop(0) # 引数の先頭は script自身の為、削除

    access_summary = {}
    access_summary_2 = {}
    
    for access_log_gz in access_log_gzs:
        f_in = gzip.open(access_log_gz, 'rt') # gzipをtextとして読取り

        i = 0
        for log_line in f_in.readlines():
            log_cols = parse_apache_log_line(log_line)
            
            if log_cols == None: continue

            # 404 や 500 errorは集計対象外
            if(log_cols['status'][0:1] == '4' or
               log_cols['status'][0:1] == '5'):
                continue
            # css や js 、 画像は集計対象外
            if is_aggregate_target(log_cols['resource']) == False:
                continue

           
            dt_str = log_cols['time'].strftime('%Y-%m')
#            dt_str = log_cols['time'].strftime('%Y-%m-%d')
            if (dt_str in access_summary ) == False:
                access_summary[dt_str] = 0
            access_summary[dt_str] += 1

            resource = log_cols['resource']
            
            if (dt_str in access_summary_2 ) == False:
                access_summary_2[dt_str] = {}
            if (resource in access_summary_2[dt_str] ) == False:
                access_summary_2[dt_str][resource] = 0
            access_summary_2[dt_str][resource] += 1
                
            
            i += 1
#            if i > 5: break


    # 集計結果を画面表示
    for date_str in access_summary_2.keys():
        for resource,count in access_summary_2[date_str].items():
            print(date_str,resource,count)

    for date_str,count in access_summary.items():
        print(date_str,count)

            
def is_aggregate_target(resource):
    # login前のtopページ系は対象外
    if (resource == '/' or
        resource[0:2] == '/?' or
        resource == '/index.html' or
        resource == '/owner/index.html' or
        resource == '/owner/login.html'):
        return False

    match_result = re_ext.match(resource)
    if match_result:
        return False

    # aws等のmetaデータ取得用(169.254.169.254)等は無視
    if (resource[0:7] == 'http://'):
        return False
    
    return True


def parse_apache_log_line(log_line):
    match_result = re_log_line.match(log_line)
    if match_result == None:
        return None
            
    log_cols = {'host'    :match_result.group(1),
                'ident'   :match_result.group(2),
                'user'    :match_result.group(3),
                'time'    :match_result.group(4),
                'method'  :match_result.group(5),
                'resource':match_result.group(6),
                'proto'   :match_result.group(7),
                'status'  :match_result.group(8),
                'bytes'   :match_result.group(9),
                'referer' :match_result.group(10),
                'agent'   :match_result.group(11) }
    
    match_result = re_time.match(log_cols["time"] )
    if match_result == None:
        return None

    month = month_def[match_result.group(2)]

    log_cols["time"] = datetime.datetime(int(match_result.group(3)),
                                         int(month),
                                         int(match_result.group(1)),
                                         int(match_result.group(4)),
                                         int(match_result.group(5)),
                                         int(match_result.group(6)))
    return log_cols

    
if __name__ == '__main__':
    main()

Webシステム/Webアプリケーションセキュリティ要件書 by OWASP

以下にて公開されている  「Webシステム/Webアプリケーションセキュリティ要件書 3.0」 は、分かりやすく、このまま利用できそう。

さすが、OWASP といった印象。

GitHub - ueno1000/secreq: Webシステム/Webアプリケーションセキュリティ要件書

政府系としては、経産省による次のページもあり、 この中で、セキュリティ診断ツールや診断企業が紹介されています。

情報セキュリティ政策(METI/経済産業省)

HTTP の REST APIで用いられる PUT や DELETE の METHOD を POST に変換

昔?のWEBアプリは、GET や POST のHTTP METHOD のみで動作できるものが多いと思います。

また、OPTIONS や TRACE の METHODは、セキュリティ的に非推奨であることから WAFやApacheにて、許可するHTTP METHOD を HEAD GET POST のみに 制限しているケースもあると思います。

REST APIWEBサービスでは、GET や POST に加え、PUT や DELETE を使いますが、 過去からの経緯が影響し、PUT や DELETE を利用できないケースもあると思います。

このようなケースは、世の中的に FAQ らしく、 X-HTTP-Method-Override や x-tunneled-method で METHOD を上書きするようです。

Step1 - client側で PUT->POSTに変更

例えば、 vue.js + axios で、X-HTTP-Method-Override による METHOD 変換を行う場合、次のように書けそうです。

ce.a.defaults.headers["X-Requested-With"] = "XMLHttpRequest";

ce.a.put = function(url, data, config) {        ## here
    config["method"] =  'post';                           ## here
    config["url"] =     url;                              ## here
    config["data"] =    data;                             ## here
    config["headers"] = {'X-HTTP-Method-Override':'PUT'}; ## here
    return this.request(config);                          ## here
};

Step2 - server側で POST->PUTに逆変換

ApacheのProxy機能で実現できるかもしれませんが、 Plack/PSGI for perlPlack::Middleware::MethodOverride - Override REST methods to Plack apps via POST - metacpan.org では、 次のように実装されています。

use Plack::Request ();

package Plack::Middleware::MethodOverride;
$Plack::Middleware::MethodOverride::VERSION = '0.20';

use parent 'Plack::Middleware';
use Plack::Util::Accessor 'param';

my %allowed_method =
    map { $_ => undef } qw(GET HEAD PUT DELETE OPTIONS TRACE CONNECT PATCH);

sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);
    $self->{param}  = 'x-tunneled-method'      unless exists $self->{param};
    $self->{header} = 'X-HTTP-Method-Override' unless exists $self->{header};
    $self->header($self->{header}); # munge it
    return $self;
}

sub call {
    my ($self, $env) = @_;
    my $meth = $env->{'plack.original_request_method'} = $env->{REQUEST_METHOD};

    if ($meth and uc $meth eq 'POST') {
        no warnings 'uninitialized';
        my $override = uc (
            $env->{$self->header}
            or $env->{QUERY_STRING} && Plack::Request->new($env)->query_parameters->{$self->param}
        );
        $env->{REQUEST_METHOD} = $override if exists $allowed_method{$override};
    }

    $self->app->($env);
}

sub header {
    my $self = shift;

    return $self->{header}      if not @_;
    return $self->{header} = '' if not $_[0];

    (my $key = 'HTTP_'.$_[0]) =~ tr/-a-z/_A-Z/;
    return $self->{header} = $key;
}

1;

他 - Movable Type の Data API の 例

Movable Type の Data API のように Request パラメータに「__method=PUT」に付与することで POST で 代替できるものもあります。

Movable Type Data API v4

setting postfix ver.3.5.2 on centos 8

install postfix ver.3.5.2 from source to centos 8 - end0tknr's kipple - 新web写経開発

先日のエントリの続きです。

今回、postfixの設定や自動起動まで実施していますが、 テスト送信を行ったところ、「status=deferred」により送信完了していません。

なので、再度、改めて、続きの調査&対応を行います。そのうち...

参考書籍

Postfix 実践入門:書籍案内|技術評論社

postfix設定

Postfix 実践入門」の第4章を参考に設定

$ sudo vi /usr/local/postfix/etc/postfix/main.cf

起動テストと、起動/停止

$ sudo /usr/local/postfix/sbin/postfix check
postfix:   warning: smtputf8_enable is true, but EAI support is not compiled in
postsuper: warning: smtputf8_enable is true, but EAI support is not compiled in

null-i.net - ぬるいねっと

上記によれば、libicu が見つからないことが原因らしい。 ( src からのinstall時に指定したつもりではいました ) 今回は /usr/local/postfix/etc/postfix/main.cf 内で smtputf8_enable=no とすることで回避。

postfix check」後、以下で、起動/停止

$ sudo /usr/local/postfix/sbin/postfix start
postfix/postfix-script: starting the Postfix mail system

$ sudo /usr/local/postfix/sbin/postfix stop
postfix/postfix-script: stopping the Postfix mail system

自動起動

10.6. systemd のユニットファイルの作成および変更 Red Hat Enterprise Linux 7 | Red Hat Customer Portal

[Linux] [systemctl] 各サービスの起動(service)ファイル一覧 - noknow

上記を参考に以下のようにしました。

$ sudo vi /etc/systemd/system/postfix.service

[Unit]
Description=Postfix Mail Transport Agent
After=syslog.target network.target

[Service]
Type=forking
ExecStart=/usr/local/postfix/sbin/postfix start
ExecReload=/usr/local/postfix/sbin/postfix reload
ExecStop=/usr/local/postfix/sbin/postfix stop

[Install]
WantedBy=multi-user.target
$ sudo systemctl enable  postfix.service
Created symlink /etc/systemd/system/multi-user.target.wants/postfix.service
  → /etc/systemd/system/postfix.service.

$ sudo systemctl start postfix.service

テスト送信

試しに自分自身に送信します

$ {
  echo "From: end0tknr"
  echo "To: end0tknr"
  echo "Subject: test postfix sendmail command"
  echo
  echo "test postfix sendmail command body"
} | /usr/local/postfix/sbin/sendmail -i -f end0tknr end0tknr

実行すると、「status=deferred」となっている為、未送信の状態となっていることがわかります。 未送信メールは、/var/spool/postfix/ に溜まっていますが、改めて調査 & 対応します。

$ sudo tail -f /var/log/maillog
   :
Jun 12 09:35:04 cent80 postfix/pickup[2933]: 0A531624D5BF: uid=1000 from=<end0tknr>
Jun 12 09:35:04 cent80 postfix/cleanup[3178]: 0A531624D5BF: message-id=<20200612003504.0A531624D5BF@cent80.a5.jp>
Jun 12 09:35:04 cent80 postfix/qmgr[2934]: 0A531624D5BF: from=<end0tknr@cent80.a5.jp>, size=347, nrcpt=1 (queue active)
Jun 12 09:35:04 cent80 postfix/local[3180]: error: open database /etc/aliases.db: No such file or directory
Jun 12 09:35:04 cent80 postfix/local[3180]: warning: dict_nis_init: NIS domain name not set - NIS lookups disabled
Jun 12 09:35:04 cent80 postfix/local[3180]: warning: hash:/etc/aliases is unavailable. open database /etc/aliases.db: No such file or directory
Jun 12 09:35:04 cent80 postfix/local[3180]: warning: hash:/etc/aliases: lookup of 'end0tknr' failed
Jun 12 09:35:04 cent80 postfix/local[3180]: 0A531624D5BF: to=<end0tknr@cent80.a5.jp>, orig_to=<end0tknr>, relay=local, delay=0.05, delays=0.03/0.01/0/0.01, dsn=4.3.0, status=deferred (alias database unavailable)

install postfix ver.3.5.2 from source to centos 8

だいぶ久しぶりに postfix の install. (postfixの設定は気が向いたら後日)

参考url

step1: download postfix

$ wget http://mirror.postfix.jp/postfix-release/official/postfix-3.5.2.tar.gz
 or
$ wget https://github.com/vdukhovni/postfix/archive/v3.5.2.tar.gz

$ tar -xvf postfix-3.5.2.tar.gz
$ cd postfix-3.5.2
$ less INSTALL

step2: required software

Oracle Berkeley DB and Cyrus SASL は、次の以前のエントリを参照。

install openldap 2.4.48 from src to centos8 for openam - end0tknr's kipple - 新web写経開発

ICU は、やはり次の以前のエントリを参照。

install php ver.7.3.15 and wordpress ver.5.3.2 - end0tknr's kipple - 新web写経開発

step3: add user and group

$ sudo useradd -c "Postfix User" -M -s /sbin/nologin postfix
$ id postfix
uid=1006(postfix) gid=1008(postfix) groups=1008(postfix)

$ sudo groupadd postdrop
$ cat /etc/group | grep post
postgres:x:1005:
postfix:x:1008:
postdrop:x:1009:

step4: make

$ make makefiles \
  CCARGS="-DUSE_SASL_AUTH -DUSE_CYRUS_SASL -DUSE_TLS -DHAS_MYSQL
          -I/usr/local/include \
      -I/usr/local/BerkeleyDB.5.1/include \ 
      -I/usr/local/cyrus_sasl/include \
      -I/usr/local/openssl_1_1_1/include \
      -I/usr/local/mysql/include" \
  AUXLIBS="-L/usr/local/lib \
       -L/usr/local/BerkeleyDB.5.1/lib \ 
           -L/usr/local/cyrus_sasl/lib \
       -L/usr/local/mysql/lib \
       -lsasl2 -lssl -lcrypto -lmysqlclient -lz -lm"
  :
No <db.h> include file found.
Install the appropriate db*-devel package first.
make: *** [Makefile.in:33: Makefiles] Error 1
make: *** [Makefile:22: makefiles] Error 2

上記のようなerrorが表示される為、DB_README に従い、makedefs を編集後、 改めて make makefiles ~ 実行。

$ less REAAME_FILES/DB_README
    :
Warning: some Linux system libraries use Berkeley DB. If you compile Postfix
with a non-default Berkeley DB implementation, then every Postfix program will
dump core because either the system library or Postfix itself ends up using the
wrong version.

On Linux, you need to edit the makedefs script in order to specify a non-
default DB library. The reason is that the location of the default db.h include
file changes randomly between vendors and between versions, so that Postfix has
to choose the file for you.
$ vi makedefs
 Linux.[345].*) SYSTYPE=LINUX$RELEASE_MAJOR
                case "$CCARGS" in
                 *-DNO_DB*) ;;
                 *-DHAS_DB*) ;;
                 *) if [ -f /usr/include/db.h ]
                    then
                        : we are all set
                    elif [ -f /usr/include/db/db.h ]
                    then
                        CCARGS="$CCARGS -I/usr/include/db"
                    elif [ -f /usr/local/BerkeleyDB.5.1/include/db.h ]            ## ADD
                    then                                                          ## ADD
                        CCARGS="$CCARGS -I/usr/local/BerkeleyDB.5.1/include/db.h" ## ADD
                    else
                        # On a properly installed system, Postfix builds
                        # by including <db.h> and by linking with -ldb
                        echo "No <db.h> include file found." 1>&2
                        echo "Install the appropriate db*-devel package first." 1>&2
                        exit 1
                    fi


$ make makefiles \
  CCARGS="-DUSE_SASL_AUTH -DUSE_CYRUS_SASL -DUSE_TLS -DHAS_MYSQL \
          -I/usr/local/include -I/usr/local/BerkeleyDB.5.1/include \
      -I/usr/local/cyrus_sasl/include/sasl \
      -I/usr/local/openssl_1_1_1/include -I/usr/local/mysql/include" \
  AUXLIBS="-L/usr/local/lib -L/usr/local/BerkeleyDB.5.1/lib \
           -L/usr/local/cyrus_sasl/lib -L/usr/local/mysql/lib \
       -lsasl2 -lssl -lcrypto -lmysqlclient -lz -lm" \
  shared=yes\
  dynamicmaps=yes\
  install_root=/\
  config_directory=/usr/local/postfix/etc/postfix\
  command_directory=/usr/local/postfix/sbin\
  daemon_directory=/usr/local/postfix/libexec\
  data_directory=/usr/local/postfix/var/lib\
  html_directory=no\
  mail_owner=postfix\
  mailq_path=/usr/local/postfix/bin/mailq\
  manpage_directory=/usr/local/postfix/man\
  newaliases_path=/usr/local/postfix/bin/newaliases\
  queue_directory=/var/spool/postfix\
  readme_directory=no\
  sendmail_path=/usr/local/postfix/sbin/sendmail\
  setgid_group=postdrop\
  shlib_directory=/usr/local/postfix/lib\
  meta_directory=/usr/local/postfix/etc/postfix

※視認性の為、CCARGS や AUXLIBS の引数には改行を入れていますが
 make時に失敗しますので、実際には、半角スペース1コで各値を区切って下さい


$ make
  :
Wformat -Wno-comment -fcommon -fPIC -g -O -I. -DLINUX4 -c dict_nis.c
cc1: warning: /usr/local/BerkeleyDB.5.1/include/db.h: not a directory
dict_nis.c:42:10: fatal error: rpcsvc/ypclnt.h: No such file or directory
 #include <rpcsvc/ypclnt.h>
          ^~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:206: dict_nis.o] Error 1
make: *** [Makefile:109: update] Error 1


## centos8 glibc には rpcsvc/ypclnt.h (libnsl)が付属しない為
## 「sudo yum install libnsl2-devel 」を実行.
## (--enablerepo=PowerTools オプションも必要かも)
## refer to https://centosfaq.org/centos/compiling-latest-postfix-fails-on-c8/

$ sudo yum install  libnsl2-devel 

$ make install


bin/postconf: error while loading shared libraries: libmysqlclient.so.21:
   cannot open shared object file: No such file or directory

make installでerrorとなった為、以下を実施

$ sudo ln -s  /usr/local/mysql/lib/libmysqlclient.so.21 /usr/lib64/


## postfix の make install は対話式で
## 様々なパラメータを入力する必要がありますが、詳細は次のurlが分かりやすいです。
## https://qiita.com/kotazuck/items/f13b59771f6241fde39d