投稿者: atmark

  • Python のテンプレートエンジン Jinja2 入門の入門

    CGI を書いていると出力する HTML の中で定型の部分が多くあることに気付きます。例えば <!DOCTYPE html> から </head> までは、どのページでもそう多く変化することはないと思います。

    CGI という仕組み上、最終的な出力は殆どが HTML です。そこに着目したのが PHP という言語で HTML の中にコードを埋めるという逆の発想で、これはこれで理に適っている仕組みです。静的なページに近い場合はこれでも良いかなと思いますが、ロジックを組むことが多いのでやっぱり Python で書きたい、でも保守性も考えたいとなったときに活躍するのがテンプレートエンジンです。

    pip が使えたら、とりあえずインストールします。

    $ pip install jinja2

    jinja2.Template

    最も基本的な使い方、単純に placeholder として使う場合です。

    from jinja2 import Template
    
    t = Template('こんにちは、{{ name }}さん')
    
    print(t.render(name = 'foo'))
    print(t.render(name = 'bar'))

    実行結果

    こんにちは、fooさん
    こんにちは、barさん

    {{ name }} のスペースをなくして {{name}} としても結果は同じですが、公式ドキュメントは全てスペースありで記述されているので統一します。

    jinja2.Environment, jinja2.FileSystemLoader

    HTML をプログラムから分離するのが今回のゴールなので、テンプレートは別ファイルとして用意します。ファイルからテンプレートを読み込むのに FileSystemLoader を使います。

    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader = FileSystemLoader('./templates'))
    
    t = env.get_template('base.tpl')
    
    print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))

    templates/base.tpl

    <title>{{ title }}</title>
    <body>{{ body }} </body>

    実行結果

    <title>Sample Page</title>
    <body><p>This is a Jinja2 sample page.</p></body>

    エスケープ処理

    鋭い方はもうお気づきだと思いますが、先の例では <p> タグがそのままテンプレートに埋め込まれています。これでは XSS の危険性があるので、エスケープ処理をする必要があります。Jinja2 には自動でエスケープ処理をしてくれる仕組みがあります。

    先程のソースの Environment を作成するときに autoescape を指定するだけです。

    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader = FileSystemLoader('./templates'), autoescape = True)
    
    t = env.get_template('base.tpl')
    
    print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))

    実行結果

    <title>Sample Page</title>
    <body>&lt;p&gt;This is a sample page.&lt;/p&gt;</body>

    select_autoescape

    select_autoescape() という、テンプレートの拡張子で autoescape をするかどうか決める仕組みもあります。import select_autoescape を加えて Environment の引数に autoescape = select_autoescape(('html.', 'xml')) とすれば、テンプレートの拡張が html か xml のときだけエスケープが働きます。

    テンプレート側で個別にエスケープ

    漏れが出たときに XSS のリスクとなるので避けたいところですが、テンプレート側で一つ一つエスケープする方法もあります。

    {{ body }}
    {{ body|escape }}
    {{ body|e }}
    <p>This is a sample page.</p>
    &lt;p&gt;This is a sample page.&lt;/p&gt;
    &lt;p&gt;This is a sample page.&lt;/p&gt;

    autoescape の部分的な有効化、無効化

    基本的に autoescape は有効にしておくべきですが、エスケープしてほしくない箇所があった場合は override して部分的に有効化および無効化できます。

    テンプレート側の {% autoescape true %}{% endautoescape %} で挟まれば部分は有効化され、逆に {% autoescape false %}{% endautoescape %} で挟まれた部分は無効化されます。

    テンプレートの書式

    ここまでは Python 側の話でしたが、ここからはテンプレート側の話です。

    {# … #} コメント

    Python の # と同じでコメントを記述できます。

    {% … %} 制御文

    for, if, macro/call, filter といった制御文を記述します。例えば for をつかって 1 から 5 までの連続した数値をリストアイテムとして出力する場合は次のようになります。

    <ul>
    {% for i in range(1, 6) %}
    {{ i }}
    {% endfor %}
    </ul>

    出力

    <ul>
    
    <li>1</li>
    
    <li>2</li>
    
    <li>3</li>
    
    <li>4</li>
    
    <li>5</li>
    
    </ul>

    元々 {% … %} があった部分は空行になって出力されます。空行の抑制は後述します。

    {{ … }} 値の出力

    変数の出力で使いますが、計算式を入れて計算結果を表示したりもできます。組込みのフィルタも多数用意されているため、値の加工も容易です。

    辞書から値を取出すのに . 区切り形式を使えます。Python で dict['key'] とするところを dict.key で表現できます。

    空白と空行の制御

    次のテンプレートと出力を基準として trim_blocks, lstrip_block, {%- … %} を指定したときの出力の違いを観察します。

    <ul>
        {% for i in range(1, 6) %}
        <li>{{ i ** 2 }}</li>
        {% endfor %}
    </ul>
    <ul>
    
        <li>1</li>
    
        <li>4</li>
    
        <li>9</li>
    
        <li>16</li>
    
        <li>25</li>
    
    </ul>

    trim_blocks = True, lstrip_blocks = False

    <ul>
            <li>1</li>
            <li>4</li>
            <li>9</li>
            <li>16</li>
            <li>25</li>
        </ul>

    trim_blocks = False, lstrip_blocks = True

    <ul>
    
        <li>1</li>
    
        <li>4</li>
    
        <li>9</li>
    
        <li>16</li>
    
        <li>25</li>
    
    </ul>

    trim_blocks = True, lstrip_blocks = True

    <ul>
        <li>1</li>
        <li>4</li>
        <li>9</li>
        <li>16</li>
        <li>25</li>
    </ul>

    {%- … %}

    trim_blocks = False, lstrip_blocks = False とした上で、ソースを下記のように変更。

    <ul>
        {%- for i in range(1, 6) %}
        <li>{{ i ** 2 }}</li>
        {%- endfor %}
    </ul>
    <ul>
        <li>1</li>
        <li>4</li>
        <li>9</li>
        <li>16</li>
        <li>25</li>
    </ul>

     参考

  • jQuery + WebStorage でフォームを自動保存する

    Web でフォームを入力中に文字を消そうと Backspace を叩いたつもりが、テキストボックスからフォーカスが外れていて Web ブラウザのページ遷移が発生してしまって全て消えたという経験は誰しもが一度はあると思います。

    最近の Web ブラウザの中には、フォーム入力中でページ遷移が発生しそうになると事前に注意をしてくれるものもあります。

    そういった環境でなくても、JavaScript で定期的にフォームの内容を自動的に保存してしまおうというのが今回の趣旨です。

    実装

    <script type="text/javascript">
    $(function() {
        // Web Storageにフォームを保存
        function saveForm() {
            // Key Valueでid, valueを格納
            let f = {};
            $("input").each(function () {
                f[$(this).attr("id")] = $(this).val();
            });
            
            // 辞書をJSON文字列化してWebStorageに保存
            localStorage.setItem("form", JSON.stringify(f));
        }
     
         // Web Storageからフォームを読込み
        function loadForm() {
            let f = JSON.parse(localStorage.getItem("form"));
            for (key in f)
                $("#" + key).val(f[key]);
        }
            
        // 前回の入力を読み込む
        loadForm();
        
        // 自動保存のインターバルタイマー(5秒毎)
        setInterval(saveForm, 5000);
    });
    </script>

    解説

    WebStorage は key = value 形式で情報を保存します。セッション限り有効な sessionStorage と永続的な localStorage があります。今回は、間違ってウィンドウを閉じても復元したいので localStorage を選びました。

    localStorage.setItem(key, value);
    localStorage.getItem(key);

    form 要素内の input 要素を順に走査して key, value に id, value の形式で保存すれば良いのですが、WebStorage を他の用途に使うことも考えられるので、JSON にして “form” という key に保存することにしました。

    let f = {};
    $("input").each(function () {
        f[$(this).attr("id")] = $(this).val();
    });

    これで一旦全ての input 要素を id = value 形式で辞書に格納しています。注意点としては id 属性を key としているので、id 属性がないと保存されません。ここは name 属性にしたり、適宜変更する形になると思います。

    localStorage.setItem("form", JSON.stringify(f));

    辞書を JSON 文字列化して一つの key に保存しています。これで無駄に key を消費せずに保存できます。

    JSON で保存したので、読み込むときも JSON として処理する必要があります。

    let f = JSON.parse(localStorage.getItem("form"))

    id 属性で拾った理由は、値をフォームにセットし直すときにセレクタが簡易に書けるからです。

    for (key in f)
        $("#" + key).val(f[key]);

    定期的に自動で保存するために、インターバルタイマーで 5 秒置きに saveForm() を呼び出すようにしました。

    setInterval(saveForm, 5000);

    これでページが読み込まれてから 5 秒置きに自動でフォームの内容が保存されるようになりました。

    参考

  • CGI でレスポンス最速はどの言語なのか

    まだしぶとく WSGI に移行せずに CGI を書いています。ちょっと気になったので、言語によって CGI の応答速度はどれくらい違うのか、調べてみました。

    環境

    • ConoHa VPS 512 MB
    • Debian 9 Stretch x64

    言語は次の通り。

    • C (gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1))
    • JavaScript (Node.js v10.10.0)
    • Python2 (Python 2.7.13 (default, Nov 24 2017, 17:33:09) [GCC 6.3.0 20170516] on linux2)
    • Python3  (Python 3.5.3)
    • Perl (v5.24.1)

    測定方法

    いずれの言語も次のような内容を出力するだけの最も簡単な CGI としました。

    Content-type: text/html; charset=utf-8
    
    <p>Hello, world! written in *.</p>
    
    

    応答時間は Chrome の開発者ツールで Network タブの表示を見ました。「リクエスト開始からレスポンスの最終バイトを受け取るまでにかかった合計時間」だそうです。

    結果

    言語時間 [ms]
    Perl28
    C29
    Python238
    Python346
    JavaScript95

    まとめ

    Perl が C と拮抗するとは意外でした。昔は CGI といえば Perl の時代でしたから、それだけ最適化が進んでいるのでしょうか。

    今回は単純に 2 行のテキスト出力を行っているだけですが、これがもっと複雑な処理を行うとなると結果は異るでしょう。参考程度に。

  • Postfix を MTA 間 TLS 通信に対応させる (Debian 9 Stretch)

     前回の記事の続きです。前回までの内容でクライアントからサーバまでの通信は SSL による暗号化が成されるようになりましたが、自サーバから相手サーバまでの通信経路はまだ平文のままです。

    Postfix の設定を少し変えるだけで、サーバ間の通信を TLS に対応させることができます。Debian 9 Stretch 環境での設定例を示します。

    /etc/postfix/main.cf

    ...
    smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
    smtp_tls_security_level = may
    smtp_tls_loglevel = 1
    ...

    “smtp_tls_CAfile” には接続先を検証するためのルート証明書を指定します。サーバ証明書は関係ないので間違えないように。

    “smtp_tls_security_level” は “may” に設定すると可能な場合は TLS を、それ以外なら通常の SMTP で送信します。”encrypt” とすると TLS を強制できますが、対応してないサーバがあると中断されます。

    ルート証明書の更新

    念のためルート証明書の更新も行っておきます。

    # update-ca-certificates
    Updating certificates in /etc/ssl/certs...
    0 added, 0 removed; done.
    Running hooks in /etc/ca-certificates/update.d...
    
    done.
    done.

    検証とログの確認

    検証は Gmail 宛にメールを送ると簡単です。Gmail で受信したメールには送信者が TLS を使用したかが簡単にわかるようにアイコンが表示されます。以下は iOS の Gmail アプリの例です。

    サーバ側のログも確認します。ルート証明書が正しく指定されていないと、次のように “Untrusted TLS connection established” のログが残ります。通信自体は暗号化されていますが、接続先の証明書が検証できなかったことを示しています。

    /var/log/mail.log

    ...
    Sep 18 10:43:52 150-95-153-XXX postfix/smtp[25050]: Untrusted TLS connection established to gmail-smtp-in.l.google.com[108.177.97.26]:25: TLSv1.2 with cipher
 ECDHE-RSA-CHACHA20-POLY1305 (256/256 bits)
    ...

    正しいルート証明書による接続先の検証が成功した場合は、次のように “Trusted TLS connection established” が残ります。

    ...
    Sep 18 10:53:36 150-95-153-XXX postfix/smtp[25506]: Trusted TLS connection established to gmail-smtp-in.l.google.com[64.233.189.27]:25: TLSv1.2 with cipher ECDHE-RSA-CHACHA20-POLY1305 (256/256 bits)
    ...

    参考

  • Let’s encrypt + Postfix で SMTP-Auth over SSL なメールサーバ構築 (Debian 9 Stretch)

    事前に DNS の設定を済ませておきます。今回は example.com の MX レコードを mail.example.com に設定してあります。

    環境

    • OS: Debian 9 (Stretch)
    • VPS: ConoHa 512 MB

    Postfix のインストール

    # apt install postfix
    apt install postfix
    apt install postfix

    “Postfix Configuration” では “Internet Site” を選択します。

    Postfix の設定

    設定項目が多すぎてどこを変更したのかわからなくなるので、元のファイルを残しておきます。

    # cp /etc/postfix/main.cf /etc/postfix/main.cf.backup
    # cp /etc/postfix/master.cf /etc/postfix/master.cf.backup

    /etc/postfix/main.cf

    ...
    # SASL
    smtpd_sasl_auth_enable = yes
    smtpd_sasl_type = dovecot
    smtpd_sasl_path = private/auth
    
    # TLS parameters
    smtpd_tls_cert_file=/etc/letsencrypt/live/example.com/fullchain.pem
    smtpd_tls_key_file=/etc/letsencrypt/live/example.com/privkey.pem
    smtpd_use_tls=yes
    smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
    smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
    # MTA間TLSの設定
    smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
    smtp_tls_security_level = may
    smtp_tls_loglevel = 1
    ...
    myhostname = mail.example.com
    ...
    home_mailbox = Maildir

    追記: 「MTA 間 TLS の設定」を加えました(2018-09-18)

    /etc/postfix/master.cf

    ...
    smtps inet n - y - - 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=
     -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
    # -o milter_macro_daemon_name=ORIGINATING
    ...
    # systemctl restart postfix

    Dovecot のインストール

    POP3 は使わないので、IMAP だけインストールします。

    # apt install dovecot-imapd

    Dovecot の設定

    こちらも元のファイルは残しておきしょう。

    # cp /etc/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf.backup
    # cp /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.backup
    # cp /etc/dovecot/conf.d/10-mail.conf /etc/dovecot/conf.d/10-mail.conf.backup

    /etc/dovecot/conf.d/10-mail.cf

    ...
    mail_location = maildir:~/Maildir
    ...

    /etc/dovecot/conf.d/10-master.cf

    ...
    inet_listener imaps {
        port = 993
        ssl = yes
    }
    ...
    # Postfix smtp-auth
        unix_listener /var/spool/postfix/private/auth {
        mode = 0666
    }

    /etc/dovecot/conf.d/10-ssl.cf

    ...
    ssl = yes
    ...
    ssl_cert = </etc/letsencrypt/live/example.com/fullchain.pem
    ssl_key = </etc/letsencrypt/live/example.com/privkey.pem
    ...
    # systemctl restart dovecot

    参考文献

  • Apache2-mpm-itk でバーチャルホスト別にユーザ管理 (Debian 9 Stretch)

    目的

    サブドメインをユーザごとに割当てた Web スペースを用意したい。例えばユーザ foo と bar がいて

    • foo.example.com -> /home/foo/www
    • bar.example.com -> /home/bar/www

    のように参照する構成にしたい。加えてそれぞれのディレクトリを参照する際の Apache の実行ユーザは、ディレクトリのオーナーと揃えたい(/home/foo/www を参照するときの実行ユーザは www-data ではなく foo)。

    必要なもの

    サブドメインをそれぞれのユーザに割り当てるには VirtualHost を使います。実行ユーザをディレクトリで変更するには、apache2-mpm-itk を使って設定に AssignUserID username groupname とすればできます。

    環境

    • OS: Debian 9 (Stretch) 64bit
    • VPS: ConoHa 512MB
    • DNS: お名前.com

    DNS の設定

    ドメインを取得したところに応じて先に設定しておいてください。今回は foo.example.com の A レコードに VPS のグローバル IP アドレスを設定しました。

    インストール

    Debian 9 (Strech) の場合は apache2 と libapache2-mpm-itk を apt で install します。

    # apt install apache2 libapache2-mpm-itk
    # a2dismod mpm_evnet
    # a2enmod mpm_itk
    # systemctl restart apache2

    サブドメインの VirtualHost を設定する

    今回は foo.example.com を /home/foo/www に割当てます。最近の Debian 系の流儀に倣い、/etc/apache2/sites-available 以下に設定ファイルを作成します。

    # vi /etc/apache2/sites-available/foo.conf

    /etc/apache2/sites-available/foo.conf

    <VirtualHost *:80>
        ServerName foo.example.com
        DocumentRoot /home/foo/www
        AssignUserID foo foo
        
        <Directory /home/foo/www>
            Require all granted
        </Directory>
        
        ErrorLog ${APACHE_LOG_DIR}/foo/error.log
        CustomLog ${APACHE_LOG_DIR}/foo/access.log combined
    </VirtualHost>

    <Directory> ディレクティブは Web スペースの利用用途によって書き換えてください。例えば .htaccess による上書きを全て許可し、CGI を動かすのであれば次のようになります。

        <Directory /home/foo/www>
            AllowOverride All
            Options +ExecCGI
            AddHandler cgi-script .cgi
            Require all granted
        </Directory>

    foo.example.com へのアクセスはログを分けます。ログの保存先は予め作成しておきましょう。

    # mkdir /var/log/apache2/foo

    作ったサイトを有効にします。

    # a2ensite foo

    ここまで進んだら設定の再読込を行い、foo.example.com にアクセスできるか確認してみましょう。サブドメインが増えても新たに設定ファイル .conf を作ってここまでの手順を繰返すだけです。

    # systemctl reload apache2

     ワイルドカード証明書

    以下の方法は自動更新に対応できていません。3 ヶ月毎に同じ手順を踏む必要があります。

    続いて、今の時代は HTTPS が当り前なので SSL 証明書の設定を行います。サブドメイン毎に証明書を取得することもできますが、目的は通信の暗号化だけなのでワイルドカード証明書 (*.example.com) を設定します。

    apt install certbot で入る certbot は古くてワイルドカード証明書を取得できないので、github から最新の certbot を git clone して使います。オプションが多いので改行入れていますが、一行で実行しても構いません。

    # apt install git
    # git clone https://github.com/certbot/certbot.git
    # cd certbot
    # ./certbot-auto certonly --manual -d *.example.net \
    -m [email protected] --agree-tos \
    --manual-public-ip-logging-ok --preferred-challenges dns-01
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Plugins selected: Authenticator manual, Installer None
    Obtaining a new certificate
    Performing the following challenges:
    dns-01 challenge for example.com
    
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Please deploy a DNS TXT record under the name
    _acme-challenge.example.com with the following value:
    
    y-oBF_mUCy6EafROfLJAvnEujA2ot_xvC4zCcJKkJ8U
    
    Before continuing, verify the record is deployed.
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Press Enter to Continue

    今回はお名前.com を使っているので、お名前.com の DNS 設定を開きます。”_acme-challenge” の TXT レコードを追加して、内容を following value の値に設定します。DNS の設定が反映されまで少々時間がかかるので、まだ Enter キーは叩かないでください。

    Windows PC を使っているならコマンドプロンプトを開き、nslookup を叩いてみます。

    C:\> nslookup -type=TXT _acme-challenge.example.com
    サーバー: one.one.one.one
    Address: 1.1.1.1
    
    権限のない回答:
    _acme-challenge.example.com text =
    
     "y-oBF_mUCy6EafROfLJAvnEujA2ot_xvC4zCcJKkJ8U"

    設定した値が表示されたら Enter キーを押して進みましょう。まだ反映されてなかったら “_acme-challeng.example.com を見つけられません: Non-existent domain” と表示されます。

    Waiting for verification...
    Cleaning up challenges
    
    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at:
     /etc/letsencrypt/live/example.com/fullchain.pem
     Your key file has been saved at:
     /etc/letsencrypt/live/example.com/privkey.pem
     Your cert will expire on 2018-12-10. To obtain a new or tweaked
     version of this certificate in the future, simply run certbot-auto
     again. To non-interactively renew *all* of your certificates, run
     "certbot-auto renew"
     - If you like Certbot, please consider supporting our work by:
    
     Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
     Donating to EFF: https://eff.org/donate-le

    “Congratulations!” と出たら Let’s encrypt からの証明書取得は完了です。

    Apache に証明書を設定する

    まず SSL モジュールを有効化します。

    # a2enmod ssl

    ポートを修正し、取得した証明書を書き加えます。

    /etc/apache2/site-available/foo.conf

    <VirtualHost *:443>
    ...
        SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
        SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
    ...
    
    </VirtualHost>

    設定を再読込して完了です。

    # systemctl reload apache2

    参考文献

  • sed でスクリプトの 1 行目だけを置換える

    $ sed '1c 置換え後の内容'

    find と組合せて、拡張子が .cgi であるファイルの 1 行目を置換えるには次のようにします。

    $ find *.py | xargs sed -i '1c #!/usr/local/bin/python'

    (さらに…)