end0tknr's kipple - 新web写経開発

http://d.hatena.ne.jp/end0tknr/ から移転しました

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

ldap3 for python による ldap 検索

次のようになると思います

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import sys
from ldap3 import *
#from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES,
#                  ALL_OPERATIONAL_ATTRIBUTES, AUTO_BIND_NO_TLS, SUBTREE


def main():
    server = Server("ldap.ないしょ.co.jp", get_info=ALL)
    conn = Connection(server,
                      read_only=True,
                      auto_bind=True)
    # conn = Connection(server,
    #                   user='ないしょ',
    #                   password='ないしょ',
    #                   check_names=True,
    #                   read_only=True,
    #                   auto_bind=True)

    mail_addrs_file = sys.argv[1]

    in_f = open(mail_addrs_file, mode='r',encoding='utf-8')

    i = 0
    for line in in_f:
        i += 1
        print(i, file=sys.stderr)
        mail_addr = line.strip()

        if len(mail_addr) ==0:
            continue

        conn.search("ou=people,o=sexy-group",
                    "(mail=" + mail_addr + ")",
                    attributes = [ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
                    # attributes=['cn', 'displayName', 'description'],
                    paged_size=1)

        if len(conn.entries) == 0:
            print(mail_addr)
            continue

        entry = conn.entries[0]

        disp_cols = [mail_addr]
        for atri_key in ["uid","cn","o","ou"]:
            if (atri_key in entry) ==True:
                disp_cols.append( str( entry[atri_key]) )
            else:
                disp_cols.append("")

        atri_key ="sexyIdCardDepartment"
        if (atri_key in entry) ==True:
            disp_cols.append( str( entry[atri_key][0]) )
        else:
            disp_cols.append("")


        print( "\t".join(disp_cols) )


    in_f.close()

        
if __name__ == '__main__':
    main()

ImageMagick の compare コマンドによる画像比較

DOS> compare.exe input_img_1.png input_img_2.png  diff_img.png

基本的な実行は上記の通りで、diff_img.png に差分が出力されます。

例えば、pythonから連続的に compareを実行する場合、次のようになると思います。

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
import time

conf = {"image_magick_compare":
        "c:/Users/end0t/tmp/ImageMagick-70/compare.exe"}

def main():
    if len(sys.argv) == 4:
        in_dir_1 = sys.argv[1]
        in_dir_2 = sys.argv[2]
        out_dir  = sys.argv[3]
    else:
        print("USAGE:",sys.argv[0],"IN_DIR_1 IN_DIR_2 OUT_DIR")
        return None

    i = 0
    for filename in os.listdir(in_dir_1):
        i += 1
        print(i, filename)
        cmd_line = " ".join([conf["image_magick_compare"],
                             in_dir_1 +"/"+ filename,
                             in_dir_2 +"/"+ filename,
                             out_dir +"/"+ filename ])

        result = subprocess.call(cmd_line)
        
if __name__ == '__main__':
    main()

以下、差分出力例。赤い部分が差異部分です

f:id:end0tknr:20200602082800p:plain

selenium for python + firefoxdriver で、page全体の画面キャプチャを連続取得

selenium for python + chromedriver で、page全体の画面キャプチャを連続取得 - end0tknr's kipple - 新web写経開発

先程のエントリの別バージョン。

chrome driverを動作させてみると、画面キャプチャ10枚程度で動作が止まってしまう...

原因不明ですが、firefoxdriver を書いてみたところ、上手く動作するみたい。

しかも、先程のように画面キャプチャ用browserの別プロセス化も不要。

こちらの方がよさそ

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import getopt
import os
import re
import sys
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
import time


conf = {"img_save_dir" : os.getcwd() + "\\IMG_TMP\\" }

def main():
    
    if len(sys.argv) == 3:
        req_domain = sys.argv[1]
        req_paths_file = sys.argv[2]
    else:
        print("USAGE:",sys.argv[0],"PROTOCOL_and_FQDN HTML_PATHS_FILE")
        return None

    browser = init_browser()
    
    req_paths = load_html_paths( req_paths_file )
    browser_tmp = None
    i = 0
    for req_path in req_paths:
        i += 1
        req_url = req_domain + req_path
        print(i, req_url)
        
        # 必要に応じ、以下を comment or un-comment
        req_url = req_url.replace('.cgi.html','.cgi')

        # 大きくしたwindowを後から小さくして、画面キャプチャすると
        # 大きい画像になるようなので、一旦、小さめ?にします
        browser.set_window_size(980, 800)

        browser.get( req_url )

        size_w = browser.execute_script("return document.body.scrollWidth;")
        size_h = browser.execute_script("return document.body.scrollHeight;")
        browser.set_window_size(size_w, size_h)
        time.sleep(1)

        # 画面キャプチャ file保存先の算出
        req_path_tmp = re.sub('^/', '', req_path)
        req_path_tmp = re.sub('/', '_', req_path_tmp)
        img_save_path = conf["img_save_dir"] + req_path_tmp + ".png"

        browser.save_screenshot(img_save_path)
        
#        if i > 5:
#            break

    browser.close()


def init_browser():
    profile = webdriver.FirefoxProfile()
    options = Options()
    options.headless = True
    
    browser = webdriver.Firefox(options=options,
                                firefox_profile=profile,
                                service_log_path=os.path.devnull)
    return browser


def load_html_paths( html_paths_file ):

    paths = []
    f = open(html_paths_file, mode='r',encoding='utf-8')
    for line in f:
        pref_city = line.split()
        paths.append( line.strip() )
    f.close()

    return paths

    

if __name__ == '__main__':
    main()

selenium for python + chromedriver で、page全体の画面キャプチャを連続取得

【Python】Seleniumでページ全体のスクリーンショット撮るならマルチプロセスで! - Qiita

上記に倣い、以下のように書くと、よさそう。

ポイントは、

  • 一旦、対象のurlへアクセス後、window sizeを取得
  • その後、別processの webdriverをwindow size指定で起動し、画面キャプチャ取得

ちなみに、以下は、python for win で動作させています

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import getopt
import os
import re
import sys
# http://chromedriver.chromium.org/getting-started
from selenium import webdriver
import time
import urllib
import urllib.request
import urllib.parse


conf = {"img_save_dir" : os.getcwd() + "\\IMG_TMP\\",
        "chrome_driver": os.getcwd() + '\\chromedriver.exe',
        "chrome_options" : ["--headless",
                            "--enable-logging=False",
                            #以下、3行はSSLエラー対策らしい
                            "--ignore-certificate-errors",
                            "--disable-extensions",
                            "--disable-print-preview"]}

def main():
    
    if len(sys.argv) == 3:
        req_domain = sys.argv[1]
        req_paths_file = sys.argv[2]
    else:
        print("USAGE:",sys.argv[0],"PROTOCOL_and_FQDN HTML_PATHS_FILE")
        return None

    browser = init_browser()
    
    req_paths = load_html_paths( req_paths_file )
    i = 0
    for req_path in req_paths:
        i += 1
        req_url = req_domain + req_path
        print(i, req_url)
        
        # 必要に応じ、以下を comment or un-comment
        req_url = req_url.replace('.cgi.html','.cgi')

        # 一旦、対象のurlへアクセス後、window sizeを取得し
        # 別processの webdriverで画面キャプチャ取得
        browser.get( req_url )
        size_w = browser.execute_script("return document.body.scrollWidth;")
        size_h = browser.execute_script("return document.body.scrollHeight;")
        # browser.set_window_size(1024,h)

        # 画面キャプチャ file保存先の算出
        req_path_tmp = re.sub('^/', '', req_path)
        req_path_tmp = re.sub('/', '_', req_path_tmp)
        img_save_path = conf["img_save_dir"] + req_path_tmp + ".png"

        browser_tmp = init_browser_for_screenshot(size_w, size_h)
        
        browser_tmp.get( req_url )
        browser_tmp.save_screenshot(img_save_path)
        browser_tmp.close()
        
#        if i > 5:
#            break

    browser.close()


def init_browser():
    chopt = webdriver.ChromeOptions()
    
    for option_tmp in conf["chrome_options"]:
        chopt.add_argument( option_tmp )

    browser = webdriver.Chrome(options = chopt,
                               executable_path=conf["chrome_driver"])
    return browser

def init_browser_for_screenshot(size_w,size_h):
    chopt = webdriver.ChromeOptions()

    for option_tmp in conf["chrome_options"]:
        chopt.add_argument( option_tmp )
    chopt.add_argument("--window-size="+str(size_w)+","+str(size_h))

    browser = webdriver.Chrome(options = chopt,
                               executable_path=conf["chrome_driver"])
    return browser


def load_html_paths( html_paths_file ):

    paths = []
    f = open(html_paths_file, mode='r',encoding='utf-8')
    for line in f:
        pref_city = line.split()
        paths.append( line.strip() )
    f.close()

    return paths

    

if __name__ == '__main__':
    main()