postfixでのメール受信時ウィルスチェック(ClamAntiVirus)でメール削除、受信者への通知を行えるようにする

目的
postfixでメールを受信したときにClamAVのウィルスチェックを行い、チェックに引っかかったものはメール削除、および受信者に対してメールの送信があったことを通知できるようにする。
procmailからpythonを呼び出したのだが、procmail周りの情報が少なくてこずったのでメモを残しておきます。

postfix設定
postfixがメール受信時にprocmailを実行するようにmaibox_commandを設定しておく
・/etc/postfix/main.cf

mailbox_command = /usr/bin/procmail

procmail設定
受信者全員に対して処理を行うので/etc/procmailrcに設定を記述する。
メール受信時にclamAVの常駐プロセス(clamd)でスキャンし、スキャン結果を新規でメールヘッダ(x-virus)に追加する。
それからprocmailのレシピでメールヘッダ(x-virus)の値を見て、ウィルス判定されていたらメール削除、メール送信があったことを受信者へ通知できるようにする。

procmailからpythonスクリプトを呼び出す場合は、以下のようにprocmailのシェル呼び出しのアクションを実行する

|/usr/bin/python /var/procmail/sh/mail.python

python側では標準入力からメールヘッダ、本文の内容が入力されるので、これを使って処理する。
標準入力からの受け取り方を知らなかったのでここら辺でもちょっとてこずった、サンプルをあげると以下のようになる。

import sys
if __name__ == "__main__":
    param = sys.argv
    print param
    f=open('/home/user/mail.txt', 'w')
    lines = sys.stdin.readlines()
    for line in lines:
        f.write(line.encode('utf-8'))
    f.close()

procmail側の処理をまとめると以下のようになる。

 パスを設定
PATH=/bin:/usr/bin:/usr/local/bin
# メールボックスの設定
MAILDIR=$HOME/Maildir
DEFAULT=$MAILDIR/
# Procmailのログファイル出力先を指定
LOGFILE=$MAILDIR/procmaillog
# ロックファイルのパスを指定
LOCKFILE=$HOME/.lockmail

# Clam AntiVirusによるウィルスチェック
AV_REPORT=`/usr/bin/clamdscan  --no-summary - 2>&1| awk -F\n -v ORS=' ' '{print}'|awk '{print $NF}'`
VIRUS=`if [ "$AV_REPORT" != "OK" ]; then echo Yes; else echo No;fi`

:0fw
| formail -i "X-Virus: $VIRUS"
# Clam AntiVirusによるウィルスチェック

# Clam AntiVirusがウィルス判定したメール通知後削除
:0
* ^X-Virus: Yes
|/usr/bin/python /var/procmail/sh/mail.python `echo "$DEFAULT"`

:0
* ^X-Virus: Yes
/dev/null

pythonに対して環境変数DEFAULTを渡しているのがポイントで、これを通知メールの送付先の情報として使っています。
もともとはメールヘッダのTOを見て通知しようとしていたのですが、よくよく考えると複数人に対してメールの送信
があると1人ずつprocmail実行されるので問題だと思ってやめました。
python側は以下のようになるかと思います。
タイトルとか本文は日本語のデコードが必要になります。

# coding: UTF-8
import sys
import codecs
import smtplib
import re
from email.MIMEText import MIMEText
from email.Header import Header
from email.Utils import formatdate
from datetime import datetime

def mail_send( from_address, to_address, mail_text, charset):
    msg = MIMEText(mail_text.encode(charset),"plain",charset)
    msg["Subject"] = Header(u"ウィルスチェックによるメール削除のご連絡",charset)
    msg["From"]    = from_address
    msg["To"]      = to_address
    msg["Date"]    = formatdate(localtime=True)
    smtp = smtplib.SMTP("localhost", 25)
    smtp.sendmail(from_address,to_address,msg.as_string())
    smtp.close()

def array2text(args, mail_info):
    msg = u"ウィルスチェックにより以下メールを削除しました。\n"
    msg += u"------------------------------------\n"
    #msg += u"送信者:" + args[1].encode('utf-8') + u"\n"
    msg += u"送信者:" + ",".join(mail_info[0]).encode('utf-8') + u"\n"
    msg += u"受信者:"
    for to_address in mail_info[1]:
        msg += to_address.encode('utf-8')
    msg += u"件名:" + mail_info[2].encode('utf-8') + "\n"
    msg += u"時間:" + datetime.now().strftime("%Y/%m/%d %H:%M:%S") + "\n"
    msg += u"------------------------------------\n"
    #msg += u"本文:\n"
    #for arg in args:
    #    msg += arg.encode('utf-8') + "\n"
    #msg += u"------------------------------------\n"
    return msg

def get_mail_info(lines):
    from_address = []
    to_address = []
    Info=[from_address, to_address, ""]
    find_to = False
    for line in lines:
        if find_to is True:
            if line.find(":") > 0:
                find_to = False
            else:
                to_address.append(line)
        if re.match("^To:", line) is not None:
            to_address.append(line[3:].strip())
            find_to = True
        if re.match("^From:", line) is not None:
            from_address.append(line[5:].strip())
        if re.match("^Subject:", line) is not None:
            Info[2] = line.encode("utf8")
    return Info


if __name__ == "__main__":
    param = sys.argv
    print param
    lines = sys.stdin.readlines()
    mail_info = get_mail_info(lines)
    mail_text = array2text(param, mail_info)
    mail_receiver = param[1].split("/")[2] + "@○○○"
    mail_send( '○○○@○○○', mail_receiver, mail_text, "UTF8")

Rの環境構築(CentOS, ubuntu)

統計解析で広く用いられているRをCentOS, ubuntuにインストールして
使えるようにする。

統計解析ではPythonも人気で自力で計算処理を実装するならライブラリが豊富にある
というメリットがあるのだろうが、Rの方が初心者にも扱いやすい印象があるのでまずは
Rに慣れたいと思う。

今回は環境を構築するところまでが目的で、以下の環境でRを使えるようにする。
Ubuntu 14.04.3 LTS
CentOS Linux release 7.2.1511

CentOS R構築
まずはepelリポジトリからRをインストールする
 1.RインストールCentOS RStudio Server 構築

yum --enablerepo=epel -y install R

CentOS RStudio Server 構築
次にRのIDEであるRStduioをインストールする。
公式サイト(https://www.rstudio.com/products/rstudio/download-server/)のほうで
構築手順の説明があるので、それに従うのが安全
 1.RStudio Serverインストール

wget https://download2.rstudio.org/rstudio-server-rhel-0.99.879-x86_64.rpm
sudo yum install --nogpgcheck rstudio-server-rhel-0.99.879-x86_64.rpm

 2.動作確認

#起動
firewall-cmd --add-port=8787/tcp --permanent
firewall-cmd --reload
rstudio-server start

ブラウザでアクセスするとログイン画面が表示されるので、osユーザでログインできることを確認する

CentOS shiny Server構築
Rで作ったアプリを降下するためのshiny serverもインストールする
こちらも公式サイト(https://www.rstudio.com/products/rstudio/download-server/)に詳しい構築手順
がのっている
 1.shiny Serverインストール

sudo su - \
-c "R -e \"install.packages('shiny', repos='https://cran.rstudio.com/')\""
wget https://download3.rstudio.org/centos5.9/x86_64/shiny-server-1.4.1.759-rh5-x86_64.rpm
sudo yum install --nogpgcheck shiny-server-1.4.1.759-rh5-x86_64.rpm

 
 2.shiny server起動

firewall-cmd --add-port=3838/tcp --permanent
firewall-cmd --reload
systemctl start shiny-server

 3.動作確認
 ためしにRStduioのでshinyのプロジェクトを作成し、shiny serverにデプロイしてみる
 RStudioのメニューから File → New Project → New Directory → Shiny Web Applicationを
 実行し新規にプロジェクトを作成する。
 それから作成したプロジェクトを/srv/shiny-server/にコピーし、ブラウザでアクセスする(ポートはデフォルトだと3838になる)

Ubuntu R-studio構築
 1.dpkgダウンロード
  https://www.rstudio.com/products/rstudio/download/からdpkgをダウンロードし
  サーバにアップロードしておく
 
 3.依存解決

sudo apt-get install gdebi-core
※警告が出たら以下実行
sudo apt-get -f install
sudo apt-get install r-base
sudo apt-get install gdebi-core

  
 2.rstudio-serverインストール(公式の手順を確認するhttps://www.rstudio.com/products/rstudio/download-server/)

wget https://download2.rstudio.org/rstudio-server-0.99.879-amd64.deb
sudo gdebi rstudio-server-0.99.879-amd64.deb

 
 3.動作確認

#起動
sudo ufw allow 8787
sudo ufw reload
sudo rstudio-server start
  
#停止
sudo rstudio-server stop

 
 ○rstudio設定
  公式を確認する(http://memorandum2015.sakura.ne.jp/docs/server/configuration.html)

/etc/rstudio/rserver.conf
/etc/rstudio/rsession.conf

Ubuntu shiny Server 構築

sudo su - \
-c "R -e \"install.packages('shiny', repos='https://cran.rstudio.com/')\""
sudo apt-get install gdebi-core
wget https://download3.rstudio.org/ubuntu-12.04/x86_64/shiny-server-1.4.1.759-amd64.deb
sudo gdebi shiny-server-1.4.1.759-amd64.deb

MeCabとCaboChaをpythonから呼び出せるようにする(ubuntu, centos)

日本語で自然言語処理をする上で必須となる形態素解析、それから係り受け解析のエンジンである
MeCabとCaboChaをpythonから呼び出せるようにする。

動作確認はUbuntu 14.04.3 LTSとCentOS Linux release 7.2.1511で行いました。

1.Mecab
1.1 Ubuntu 14.04.3でMeCab環境構築
//ubuntupython-mecabのインストール
$ sudo aptitude install python-nltk python-numpy python-matplotlib python-networkx prover9
$ sudo apt-get install mecab libmecab-dev mecab-ipadic
$ sudo aptitude install mecab-ipadic-utf8
$ sudo apt-get install python-mecab

//動作確認
$ python

# coding: utf-8
import sys
import MeCab

m = MeCab.Tagger ("-Ochasen")
print ("私の名前はボブです。")
print m.parse("私の名前はボブです。")

1.2 CentOS 7.2でMeCab環境構築
mecabのインストール
$ cd /var/tmp
$ curl -O https://mecab.googlecode.com/files/mecab-0.996.tar.gz
$ tar zxfv mecab-0.996.tar.gz
$ cd mecab-0.996
$ ./configure
$ make
$ sudo make install

○辞書(ipadict)インストール
$ cd /var/tmp
$ curl -O https://mecab.googlecode.com/files/mecab-ipadic-2.7.0-20070801.tar.gz
$ tar zxfv mecab-ipadic-2.7.0-20070801.tar.gz
$ cd mecab-ipadic-2.7.0-20070801
$ ./configure --with-charset=utf8
$ make
$ sudo make install

pythonからmecabを使えるようにする
$ yum groupinstall -y 'development tools'
$ yum install python-devel
$ pyenv install anaconda2-2.4.1
$ pyenv global anaconda2-2.4.1
$ vi /etc/ld.so.conf #共有ライブラリにパスを通す

include ld.so.conf.d/*.conf
/usr/local/lib

$ curl -O https://mecab.googlecode.com/files/mecab-python-0.996.tar.gz
$ tar zxfv mecab-python-0.996.tar.gz
$ cd mecab-python-0.996
$ vi setup.py #cmd2("~の部分を絶対パスに変える

#!/usr/bin/env python

from distutils.core import setup,Extension,os
import string

def cmd1(str):
return os.popen(str).readlines()[0][:-1]

def cmd2(str):
return string.split (cmd1(str))

setup(name = "mecab-python",
#version = cmd1("mecab-config --version"),
version = cmd1("/usr/local/bin/mecab-config --version"),
py_modules=["MeCab"],
ext_modules = [
Extension("_MeCab",
["MeCab_wrap.cxx",],
#include_dirs=cmd2("mecab-config --inc-dir"),
include_dirs=[r"/usr/local/bin/mecab-config --inc-dir"],
#library_dirs=cmd2("mecab-config --libs-only-L"),
library_dirs=[r"/usr/local/bin/mecab-config --libs-only-L"],
#libraries=cmd2("mecab-config --libs-only-l"))
libraries=["libmecab"])
])

$ python setup.py build
$ sudo python setup.py install

//動作確認
python

# coding: utf-8
import sys
import MeCab

mecab = MeCab.Tagger ("-Ochasen")

print ("私の名前はボブです。")
print mecab.parse("私の名前はボブです。")

sent = u"かれのくるまでまつ".encode('utf-8')
print mecab.parse(sent)

node = mecab.parseToNode(sent)
node = node.next
while node:
print node.surface, node.feature
node = node.next


2.CaboCha
2.1 Ubuntu 14.04.3でCaboCha環境構築
//ubuntuにcabochaを入れてpythonから使う
$ sudo apt-get install mecab libmecab-dev mecab-utils mecab-ipadic-utf8
$ sudo apt-get install build-essential
$ tar zxvf CRF++-0.58.tar.gz
$ cd CRF++-0.58
$ ./configure
$ make
$ sudo make install

$ vi /etc/ld.so.conf

include /etc/ld.so.conf.d/*.conf
include /usr/local/lib

$ sudo ldconfig


cabocha-0.68.tar.bz2をサーバにアップロードしておく
$ bzip2 -dc cabocha-0.68.tar.bz2 | tar xvf -
$ cd cabocha-0.68
$ ./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
$ make
$ make check
$ sudo make install
$ cd python
$ sudo apt-get install python-dev
$ python setup.py build
$ sudo python setup.py install


pythonでcabochaをつかってみる
python

import CaboCha
cabocha = CaboCha.Parser('--charset=UTF8')
sent = u"太郎はこの本を二郎を見た女性に渡した。".encode('utf-8')
print cabocha.parseToString(sent)
tree = cabocha.parse(sent)
print tree.toString(CaboCha.FORMAT_LATTICE)
print tree.toString(CaboCha.FORMAT_XML)

2.2 CentOS 7.2でCaboCha環境構築
//CentOsにcabochaを入れてpythonから使う
○crf++のインストール(事前にファイルをあげておく)
$ tar xzf CRF++-0.58.tar.gz
$ cd CRF++-0.58
$ ./configure
$ make
$ su
# make install

○cabochaのインストール(事前にファイルをあげておく)
$ bzip2 -dc cabocha-0.66.tar.bz2 | tar xvf -
$ cd cabocha-0.66
$ ./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
$ make
$ make check
$ su
# make install
pythonからCaboChaを使えるようにする
$ cd python
$ python setup.py build
$ python setup.py install
$ ldconfig

//動作確認
$ python

import CaboCha
cabocha = CaboCha.Parser('--charset=UTF8')
sent = u"太郎はこの本を二郎を見た女性に渡した。".encode('utf-8')
print cabocha.parseToString(sent)
tree = cabocha.parse(sent)
print tree.toString(CaboCha.FORMAT_LATTICE)
print tree.toString(CaboCha.FORMAT_XML)

for i in range(tree.chunk_size()):
chunk = tree.chunk(i)
print 'Chunk:', i
print ' Score:', chunk.score
print ' Link:', chunk.link
print ' Size:', chunk.token_size
print ' Pos:', chunk.token_pos
print ' Head:', chunk.head_pos # 主辞
print ' Func:', chunk.func_pos # 機能語
print ' Features:',
for j in range(chunk.feature_list_size):
print ' ' + chunk.feature_list(j)
print
print 'Text'
for ix in range(chunk.token_pos,chunk.token_pos + chunk.token_size):
print ' ', tree.token(ix).surface
print

for i in range(tree.token_size()):
token = tree.token(i)
print 'Surface:', token.surface
print ' Normalized:', token.normalized_surface
print ' Feature:', token.feature
print ' NE:', token.ne # 固有表現
print ' Info:', token.additional_info
print ' Chunk:', token.chunk
print

メールサーバ構築

環境
  idcfクラウド上のCentOS7に環境構築
  メール送信にはサーバのドメインが必要なので、Dynamic DO!(http://ddo.jp/)を利用してクラウド
  サーバにドメインを割り当てる。
 
 メールサーバの環境構築としてMTA(Message Transfer Agent)にpostfix, MDA(Mail Deliver Agent)に
 dovecotでそれぞれ設定の設定を行う。
 
 postfixについては、CentOS7に最初からインストールされていると思うが、以下のコマンドで確認してみる。

 rpm -q postfix

 
 postfixがインストールされていなければ以下のコマンドでインストールする。

 yum install postfix

 
 次に、自動起動の設定をしておく

 systemctl enable postfix

 
 dnsと同じでpostfixchrootで起動するようにする。
 以下のコマンドでpostfixchroot化する。

 /usr/share/doc/postfix-x.x.x/examples/chroot-setup/LINUX2

 
 それからmaster.cfにchrootの設定を行う。

 vi /etc/postfix/master.cf

# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (yes) (never) (100)
# ==========================================================================
smtp inet n - y - - smtpd
#smtp inet n - n - 1 postscreen
#smtpd pass - - n - - smtpd
#dnsblog unix - - n - 0 dnsblog
#tlsproxy unix - - n - 0 tlsproxy
#submission inet n - n - - smtpd
# -o syslog_name=postfix/submission
# -o smtpd_tls_security_level=encrypt
# -o smtpd_sasl_auth_enable=yes
# -o smtpd_reject_unlisted_recipient=no
# -o smtpd_client_restrictions=$mua_client_restrictions
# -o smtpd_helo_restrictions=$mua_helo_restrictions
# -o smtpd_sender_restrictions=$mua_sender_restrictions
# -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
# -o milter_macro_daemon_name=ORIGINATING
#smtps inet n - n - - smtpd
# -o syslog_name=postfix/smtps
# -o smtpd_tls_wrappermode=yes
# -o smtpd_sasl_auth_enable=yes
# -o smtpd_reject_unlisted_recipient=no
# -o smtpd_client_restrictions=$mua_client_restrictions
# -o smtpd_helo_restrictions=$mua_helo_restrictions
# -o smtpd_sender_restrictions=$mua_sender_restrictions
# -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
# -o milter_macro_daemon_name=ORIGINATING
#628 inet n - n - - qmqpd
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
defer unix - - y - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - y 1000? 0 flush
proxymap unix - - y - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - y - - smtp
relay unix - - y - - smtp
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
showq unix n - y - - showq
error unix - - y - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
#
# ====================================================================

上記に従い、chrootにyをつけたあとpostfixの設定再読み込み、または再起動を
するとchrootでの起動になっているはずである。

systemctl restart postfix

chrootで起動しているかはpsを確認したときに、プロセスに-cのオプションがついていること
で確認できる。

ps aux | grep postfix


次にpostfixの設定ファイル/etc/postfix/main.cfを編集する。

vi /etc/postfix/main.cf

#ホスト名
myhostname = ooo.xxx.ddo.jp

#ドメイン
mydomain= xxx.ddo.jp

#メールアドレスのドメイン
myorijin = $mydomain

#SMTP接続を待ち受けるネットワークインターフェース
inet_interfaces = all

#IPv4/IPv6の動作をall/ipv4/ipv6で指定
inet_protocols = all

#ローカル階層を行うドメイン
mydestination = $myhostname, localhost.$mydomain, localhost

#中継を許可するSMTPクライアントのアドレス
mynetowrks = 192.168.xx.00/24, 127.0.0.0/8

#メールすプールディレクトリ
mail_spool_directory = /var/spool/mail

#メールボックスへのパス
home_mailbox = Maildir/

#一人当たりのメールボックス要領
mailbox_size_limit = 104857600
#mailbox_size_limit = 0

次にpostfixを再起動する。

systemctl restart postfix

それから各ユーザーのメールボックスとして/home/*/Maildirが指定されているので
まず、ユーザディレクトリの雛形にMaildirを追加する

mkdir -p /etc/skel/Maildir/{new,cur,tmp}
chmod -R 700 /etc/skel/Maildir/

すでにユーザーが作成されている場合は以下のコマンドでメールボックス用のフォルダを作成できる

find /home/ -maxdepth 1 -mindepth 1 -type d | gawk '{print $NF}' | xargs -i mkdir -p {}/Maildir/{new,cur,tmp}
find /home/ -maxdepth 1 -mindepth 1 -type d | gawk '{print $NF}' | xargs -i chmod -R 700 {}/Maildir

次にsendmailコマンドでローカルのユーザーにメールが送信されているかを確認する

sendmail □□□@xxx.ddo.jp
From: □□□@xxx.ddo.jp
To: △△△@xxx.ddo.jp
Subject: test
this is test mail.
.

これでメールが送信されるはずだが実際に受け取っているかはmailコマンドで確かめてみる
mailコマンド実行できなかったらyumでインストールする

yum install mailx

mailコマンド使えるようになったら以下のコマンドでメール送信、受信できたか確認してみる

mail -f /home/△△△/Maildir

そしたらメールが受信されていることを確認されているはずである。
メールが受信されていなかったらメールログなど確認してみる。

tail -f /var/log/maillog

Redisとspring-boot連携

Redisとは
インメモリのkey-valueストア
メモリ上にデータを格納するので,非常に高速にデータの書き込み・読み込みを行うことができる。
キャッシュの保存とかセッション管理とかに使えるかも。

Redisインストール
epelリポジトリに含まれていたからyumでインストールした

yum install redis

インストールできなかったらepelリポジトリ追加して試す

yum install epel-release
yum --enablerepo=epel install redis


redisサーバの起動

redis-server

redisクライアントの起動

redis-cli

spring boot連携
・pom.xmlに以下を追加

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

・application.propertiesにredisの接続情報を記入

spring.redis.host=localhost
#spring.redis.password=secret
spring.redis.port=6379

・Redisに値をset, getする

@Controller
public class SampleController {

	@Autowired
	private HelloWorldService helloWorldService;
	
	@Autowired
	private StringRedisTemplate template;

	@RequestMapping("/")
	@ResponseBody
	public String helloWorld(HttpServletRequest request) {
		ValueOperations<String, String> ops = this.template.opsForValue();
		String key = "spring.boot.redis.test";
		if (!this.template.hasKey(key)) {
			ops.set(key, "foo");
		}
		System.out.println("Found key " + key + ", value=" + ops.get(key));
		
		return this.helloWorldService.getHelloMessage();
	}
}

Redisに保存されているかは以下のコマンドで確認できる。

redis-cli
127.0.0.1:6379> KEYS *
1) "spring.boot.redis.test"
127.0.0.1:6379> get spring.boot.redis.test
"foo"

pythonのWebフレームワークTornadoでhttpのプロキシサーバを作ってみた

PythonではTornadoというWebフレームワークを使うことで簡単にノンブロッキングWebサーバの開発ができるらしい。

最近ではnode.jsなどがノンブロッキングWebサーバとして注目を集めているが、文字列操作の点でpythonの方が優れていそうだったのでとりあえずpythonで実装してみることにした。

○まずはTornadoをPythonにインストール
Tornadoのインストールはpipで簡単に行える

pip install tornado

pipを使わない場合はソースコードを落としてきて、直接インストールできる。

tar xvzf tornado-4.1.tar.gz
cd tornado-4.1
python setup.py build
sudo python setup.py install

○Tornadoを使ってプロキシサーバを作成
以下のコードでプロキシサーバになる

#!/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import os
import tornado.httpserver
import tornado.ioloop
import tornado.iostream
import tornado.web
import tornado.httpclient
import ssl

class ProxyHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        return self.myRequest()

    @tornado.web.asynchronous
    def post(self):
        return self.myRequest()

    def myRequest(self):
        #self.render("test.html")
        def get_response( response):
            if response.error and not isinstance(response.error,tornado.httpclient.HTTPError):
                self.set_status(500)
                self.write('500 error:\n' + str(response.error))
                self.finish()
            else:
                self.set_status(response.code)
                for header in ('Date', 'Cache-Control', 'Server', 'Content-Type', 'Location'):
                    v = response.headers.get(header)
                    if v:
                        self.set_header(header, v)
                if response.body:
                    print(self.request.uri)
                    #self.write(response.body.replace("<body".encode("utf-8"), "<script type='text/javascript'>(function(){alert('hello');})();</script><body".encode("utf-8")))
                    self.write(response.body)

                    #self.render(response.body)
                self.finish()

        req = tornado.httpclient.HTTPRequest(
            url=self.request.uri,
            method=self.request.method, body=self.request.body,
            headers=self.request.headers,
            follow_redirects=False,
            allow_nonstandard_methods=True)
        client = tornado.httpclient.AsyncHTTPClient()
        try:
            #コールバック関数にhandle_responseを指定。ここにアクセスしたレスポンスが入る
            client.fetch(req, get_response)
        except tornado.httpclient.HTTPError as e:
            if hasattr(e, 'response') and e.response:
                get_response(e.response)
            else:
                self.set_status(500)
                self.write('500 error:\n' + str(e))
                self.finish()

def run_proxy(port):
    app = tornado.web.Application(
        [(r'.*', ProxyHandler),]
    )
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(port)

    print("Server is up ...")
    tornado.ioloop.IOLoop.instance().start() #プロキシサーバを稼働させる

if __name__ == "__main__":
    port = 8888
    if len(sys.argv) > 1:
        port = int(sys.argv[1])

    print ("Starting cache proxy on port %d" % port)
    run_proxy(port)
    #run_ssl_proxy(8888)

やっていることとしては、ポートの8888でサーバを立てておきリクエストを受け取ったらをそのまま中継させるだけだ。
サーバを立ち上げているのは以下の部分になります。

    app = tornado.web.Application(
        [(r'.*', ProxyHandler),]
    )
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(port)

tornado.web.Applicationの引数で受け取るリクエストと呼びだすクラスを指定しています。
また、今回は指定していませんがSSL通信を行いたい場合は以下のようにオプションとして証明書と秘密鍵の指定が必要になります。

    app = tornado.web.Application(
        [(r'.*', ProxyHandler),],
        template_path=os.path.join(os.getcwd(),  "static"),
        static_path=os.path.join(os.getcwd(),  "static")
    )

    http_server = tornado.httpserver.HTTPServer(app, ssl_options={
        "certfile":"/var/pyTest/ca2.crt",
        "keyfile":"/var/pyTest/ca2_out.key",
        #ssl_version= ssl.PROTOCOL_TLSv1
    })

○ブラウザのプロキシとして設定する
ブラウザからアクセスする場合は、プロキシとして通す設定が必要になる。
Firefoxの場合はオプション→詳細→ネットワーク→接続設定で行うことができる。


ブラウザ設定後リクエストを飛ばしたらGET,POSTにかかわらずmyRequest()メソッドを実行するようにしています。リクエストは以下の部分で設定を行っています。内容としては受け取ったリクエストをそのままセットしているだけです。

req = tornado.httpclient.HTTPRequest(
    url=self.request.uri,
    method=self.request.method, body=self.request.body,
    headers=self.request.headers,
    follow_redirects=False,
    allow_nonstandard_methods=True)
client = tornado.httpclient.AsyncHTTPClient()

リクエストのコールバックはclient.fetch(req, get_response)で指定しています。
それからリクエストの送信とエラー時の処理を定義しています。

if hasattr(e, 'response') and e.response:
    get_response(e.response)
else:
    self.set_status(500)
    self.write('500 error:\n' + str(e))
    self.finish()

コールバックメソッド実行時結果が500エラーなら500 errorとクライアントに表示します。

if response.error and not isinstance(response.error,tornado.httpclient.HTTPError):
    self.set_status(500)
    self.write('500 error:\n' + str(response.error))
    self.finish()

結果を受け取れた場合、レスポンスヘッダとボディにそのままの内容を書き込んでいます。
self.writeの部分で文字列操作を行うと、クライアント側の表示を書き換えることができます。今回は使っておりませんが、lxmlというサードパーティモジュールがhtmlの操作に便利そうです。

else:
	self.set_status(response.code)
	for header in ('Date', 'Cache-Control', 'Server', 'Content-Type', 'Location'):
	    v = response.headers.get(header)
	    if v:
	        self.set_header(header, v)
	if response.body:
	    print(self.request.uri)
	    #self.write(response.body.replace("<body".encode("utf-8"), "<script type='text/javascript'>(function(){alert('hello');})();</script><body".encode("utf-8")))
	    self.write(response.body)

	    #self.render(response.body)
	self.finish()

結果ですが、httpプロトコルについてはうまくプロキシサーバとして動いておりましたがhttpsSSL通信を行おうとしたらエラーが発生しました。自分で準備したオレオレ証明書秘密鍵の部分でエラーが発生していたので、ちょっと調べたいと思います。

→ちょっと調べてみました。どうやらこのやり方だとクライアント→プロキシサーバ間もssl通信をする想定になっているようだが、クライアント側のFirefoxssl通信のプロキシとして指定したらクライアント→プロキシサーバ間でssl通信を行っていないようなのでエラーになってそうな気がする。
ブラウザのプロキシに指定するのではなくhttps://localhost:8888/https://google.co.jpとかでリクエストは受け取れていたので多分そうです。

まあ、httpsの場合はプロキシサーバはキャッシュの転送だけを行うルータのように動くのが前提のプロトコルのようなので、無理そうか
HTTPSプロキシの仕組み - ソフトウェアエンジニア現役続行

Spring Boot基礎

Spring Bootについて調べた内容をまとめてみる

○springアプリケーション起動
 SpringApplicatin.runを実行することで設定を読み込みアプリを起動する
 @autowiredや@ControllerアノテーションはSpringApplication.run以下のパッケージで有効になる。
 なのでアプリの根っこの部分のパッケージなどにSpringApplication.runを実行するクラスを配置させる。

○リクエストを受け取れるようにする
 まずはリクエストを受け取れるようにする。
 リクエストを受け取れるようにするには@RequestMappingアノテーションでURLのパスを指定する。
 @RequestMappingはクラスとメソッドの両方で有効なアノテーションである。
 そのため、以下のようにクラスとメソッドに@RequestMappingアノテーションを指定した場合、exec()メソッドを呼び出す場合のURLはhttp://ホスト名:ポート/login/execになる。

@Controller
@RequestMapping("/login")
public class LoginAction {
 @RequestMapping(value="/exec", method={RequestMethod.POST, RequestMethod.GET})
public String exec(HttpServletRequest request, @ModelAttribute LoginForm form, Model model) {
 	UserMstEntity user = userMstService.findUserByIdPass(form.getUserId(), form.getPassword());
 	if(isLoginSuccess( user) ){
 		//メイン画面へ遷移
 		request.getSession().setAttribute(UserMstEntity.class.getName(), user);
 		return "redirect:/main/";
 	}else{
 		model.addAttribute("msg", "ログイン情報が間違っています");
 		return "/login/index";
 	}
}

 それからjspを返す場合はクラスに@ControllerアノテーションをつけておきRequestMappingで呼び出されるメソッドの戻り値でjsp1ファイルへのパスを返すようにする。またjspファイルではなくサーバサイドのRequestMappingが指定されたメソッドを呼び出す場合はreturn "redirect:/main/";のようにredirect:でサーバサイドのメソッドのリクエストマッピングを指定するようにする。Reqiest○リクエストを受け取ったあとにJSPの画面を返せるようにする。
 spring bootでjspを使用する場合は、pom.xmlのdependenciesに以下の定義を追加する。

 <dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
	<scope>provided</scope>
 </dependency>
||< 
 jspファイルを呼び出す場合、@RequestMappingで呼び出されたメソッドが以下のようにjspファイルのパスを指定すればよい。
 return "/login/index";
 だが、その前にsrc/main/resourcesにapplication.propertesを配置し以下の内容を記述しておく必要がある。
>||
 spring.view.prefix: /WEB-INF/views/
 spring.view.suffix: .jsp

 これでjspファイルのパスのルートが/WEB-INF/views/になる。
 また、今回はpropertiesファイルを使ったがymlを使うこともできるので使いやすい方を使えばよいでしょう。

 また、サーバサイドからjspに値を渡す場合は以下のようにmodel.addAttributeをすれば良い。

@RequestMapping(value="/logout")
public String logout(HttpServletRequest request, Model model) {
	request.getSession().setAttribute(UserMstEntity.class.getName(), null);
	model.addAttribute("msg", "ログアウトしました");
	return "/login/index";
}

 model.addAttributeされた値を使うにはstrutsと同じようにel式(${msg})で指定する。

○リクエストされた値をサーバサイドで受け取れるようにする。
 クライアントからリクエストされた値を受け取る場合、簡単な方法としては以下のように@RequestParamアノテーションを使ったものがある。

RequestMapping(value="/exec", method={RequestMethod.POST, RequestMethod.GET})
public String exec(HttpServletRequest request,@RequestParam("userId") String userId,
                       @RequestParam("password") String password, Model model) {
||< 
 ひとつずつ値を受け取るのが面倒な場合はFormクラスを作っておいて求めて受け取れるようにする方法がある。
 formを使用する場合、通常だったらgetter,setterを用意しなければ行けないのだがlombockといってコンパイルしたクラスファイルに自動でgetter,setterを追加してくれる便利なライブラリがある。lombockが使用可能なら以下のようにクラスファイルに@Dataアノテーションを使用することでeclipseがコンパイルしたあとのクラスファイルファイルに手を加えてくれる。
>|java|
import lombok.Data;
@Data
public class LoginForm {
    private String userId;
    private String password;
    
    private String msg;

}

 lombockを使用するにはまず、pom.xmlに以下の定義を加える。

 <dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.16.4</version>
	<scope>provided</scope>
 </dependency>
||< 
 これで@Dataアノテーションを使ったとしてもエラーはなくなるはずである。ただこれだけでは不十分でありクラスファイルを確認してもgetter,setterメソッドがセットされていないのがわかる。@Dataアノテーションを使用する場合はeclipse自体の設定を変更しなければいけず、方法としては公式サイトからダウンロードしてきたlombock.jarを実行して使用するeclipseを指定するというものだ。これで@Dataアノテーションがついているものにはgetter,setterが自動で吹かされていることが確認できる。

 クライアントからのリクエストをFormにセットする場合は、クラスに@Dataアノテーションを記述したあと以下のように@ModelAttributeアノテーションを使う。
>|java|
 @RequestMapping(value="/exec", method={RequestMethod.POST, RequestMethod.GET})
public String exec(HttpServletRequest request, @ModelAttribute LoginForm form, Model model) {
 	UserMstEntity user = userMstService.findUserByIdPass(form.getUserId(), form.getPassword());

とりあえずこれで、クライアントとサーバサイドで基本的な情報のやり取りができるようになった。
DBと依存性周りについてはまた今度で、