KITA Eng.

北海道でサーバー技術者として歩み出したひとが綴るblog。

Windows10で「OSError: raw write() returned invalid length...」的なエラーが出たら

site-packages以下にsitecustomize.pyの以下ファイルを作って置いてあげる。

import win_unicode_console
win_unicode_console.enable()

このファイルは、最初に読み込んでくれるらしい。 https://docs.python.jp/3/library/site.html

win_unicode_console自体は入れておいてあげる必要がある。 https://pypi.python.org/pypi/win_unicode_console

pip install win_unicode_consoleでOK!

Minicondaとかで仮想環境作っているなら、 C:\Users\username\Miniconda3\envs\project1\Lib\site-packagesの下に上記のファイルを作ってあげればよい模様。

Conoha VPSでipv6利用時に気を付けたいPostfixの設定

ConohaのVPSは、ipv6が標準で、1個+16個ついてきて楽し気です。 が、思わずはまることもあります。

はまったこと

Postfixipv6を有効にしていると、Gmailなんかにはipv6でつなぎに行くのですが、 その時に以下のようなエラーが記録されて送信できないことがあります。

Mar  8 06:25:09 conoha-vps postfix/cleanup[31246]: B58C1526A: message-id=<20180307212509.B1E605846@conoha-vps>
Mar  8 06:25:09 conoha-vps postfix/qmgr[1703]: B58C1526A: from=<>, size=7598, nrcpt=1 (queue active)
Mar  8 06:25:09 conoha-vps postfix/local[31254]: B1E605846: to=<root@conoha-vps>, relay=local, delay=0.03, delays=0.01/0/0/0.01, dsn=2.0.0, status=sent (forwarded as B58C1526A)
Mar  8 06:25:11 conoha-vps postfix/smtp[31255]: B58C1526A: to=<xxxxxxxxxxx@gmail.com>, orig_to=<root@conoha-vps>, relay=gmail-smtp-in.l.google.com[2404:6800:4008:c01::1b]:25, delay=1.9, delays=0.01/0/1.2/0.7, dsn=5.7.1, status=bounced (host gmail-smtp-in.l.google.com[2404:6800:4008:c01::1b] said: 550-5.7.1 [2400:8500:1302:851:a150:95:xxx:xxx] The IP address sending this 550-5.7.1 message does not have a PTR record setup. As a policy, Gmail does not 550-5.7.1 accept messages from IPs with missing PTR records. Please visit 550-5.7.1  https://support.google.com/mail/answer/81126#authentication for more 550 5.7.1 information. 13-v6si13594494ple.157 - gsmtp (in reply to end of DATA command))
Mar  8 06:25:11 conoha-vps postfix/qmgr[1703]: B58C1526A: removed

どうやら、ipv6のときは、IPの逆引き結果が登録されていて、なおかつ正引きと一致する必要があるようです。 標準のメインとなる1個のものに対しては一致するように登録していたのですが、+16個の方がそうではなかったので、ダメだったようです。

解決策

NICへのIP割り当てが以下のようになっていて、メインの2400:8500:1302:851:150:95:xxx:xxxに対して適当な逆引きが設定されている(正引きと一致する)場合。

username@conoha-vps:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 150.95.xxx.xxx/23 brd 150.95.xxx.255 scope global ens3
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxf/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxe/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxd/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxc/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxb/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxxa/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx9/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx8/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx7/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx6/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx5/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx4/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx3/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx2/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:a150:95:xxx:xxx0/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2400:8500:1302:851:150:95:xxx:xxx/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::1:xxxx:xxxx:xxxx/64 scope link
       valid_lft forever preferred_lft forever

以下のように、/etc/postfix/main.cfに追記してあげることで、該当のipv6アドレスからGmailサーバーなどipv6対応のメールサーバーにつなぎに行ってくれます。

smtp_bind_address6 = 2400:8500:1302:851:150:95:xxx:xxx

Chromeで旧シマンテック/ジオトラスト社のSSL証明書が信頼されたものでなくなる日に向けて...

昨年から、なんだかゴタゴタしていたやつ。 長めの期間で証明書を取得していると、再発行の該当になっているパターンがある。

信頼されなくなっちゃうパターン

ref: https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html

対象1:2016年5月31日以前に発行されたSSLサーバ証明書(ラピッドSSL、クイックSSLプレミアム)

Chrome66からSSLサーバ証明書が無効化される(2018年4月17日に正式版リリース予定)

  • 有効期間が2018年3月14日以前の場合は、対応不要
  • 有効期間が2018年3月15日以降の場合は、2017年12月1日以降に再発行が必要

対象2:2016年6月1日2017年11月30日までに発行されたSSLサーバ証明書(ラピッドSSL、クイックSSLプレミアム、セキュア・サーバID、セキュア・サーバID EV、グローバル・サーバID)

Chrome70からSSLサーバ証明書が無効化される(2018年10月23日に正式版リリース予定)

  • 有効期間が2018年9月12日以前の場合は、対応不要
  • 有効期間が2018年9月13日以降の場合は、2017年12月1日以降に再発行が必要

簡易的なチェカースクリプト作った

管理対象の証明書がそれなりにあると、手作業チェックは漏れもありそうなので、確認の意味もこめて、チェックスクリプト(簡易版)を作ってみた。 Gist:kacchan822/check_cert_chrome.py 記事末尾にも載っけてある

以下の具合で、対象1または2で対応が必要だと、それぞれCRITICAL-1CRITICAL-2と教えてくれる。対象1または2だけど、対応不要な場合も、一応WARNINGになる。

$ python3 check_cert_chrome.py www.example.com
[     OK     ] www.example.com: Cert was published at 2017-12-15 20:03:18. (Let's Encrypt, Let's Encrypt Authority X3)


$ python3 check_cert_chrome.py www.example.com
[  WARNING   ] www.example.com: Cert was published at 2016-03-16 00:00:00; before 2016-05-31. (GeoTrust Inc., RapidSSL SHA256 CA)

$ python3 check_cert_chrome.py www.example.com
[  WARNING   ] www.example.com: Cert was published at 2017-06-10 00:00:00; between 2016-06-01 and 2017-11-30. (Symantec Corporation, Symantec Class 3 Secure Server CA - G4)


$ python3 check_cert_chrome.py www.example.com
[ CRITICAL-1 ] www.example.com: Cert was published at 2016-03-16 00:00:00; before 2016-05-31. (GeoTrust Inc., RapidSSL SHA256 CA)

$ python3 check_cert_chrome.py www.example.com
[ CRITICAL-2 ] www.example.com: Cert was published at 2017-04-01 00:00:00; between 2016-06-01 and 2017-11-30. (Symantec Corporation, Symantec Class 3 Secure Server CA - G4)

すべての証明書を正しく判定できるわけではないので、あしからず…。

Gist:kacchan822/check_cert_chrome.py

#!/usr/bin/env python3
# This software is released under the MIT License.
# http://opensource.org/licenses/mit-license.php

import datetime
import re
import socket
import ssl
import sys

timeout = 5


def get_cert(cn, port=443):
    """ 
        return: dict
            {'OCSP': ('http://ocsp.int-x3.letsencrypt.org',),
            'caIssuers': ('http://cert.int-x3.letsencrypt.org/',),
            'issuer': ((('countryName', 'US'),),
                        (('organizationName', "Let's Encrypt"),),
                        (('commonName', "Let's Encrypt Authority X3"),)),
            'notAfter': 'Mar 15 20:46:19 2018 GMT',
            'notBefore': 'Dec 15 20:46:19 2017 GMT',
            'serialNumber': '0467F6AD9DE0A1D776DC5688B22569094A49',
            'subject': ((('commonName', 'ip.ksn.cloud'),),),
            'subjectAltName': (('DNS', 'ip.ksn.cloud'),
                                ('DNS', 'ip4.ksn.cloud'),
                                ('DNS', 'ip6.ksn.cloud')),
            'version': 3}        
    """
    context = ssl.create_default_context()
    try:
        with context.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM),
                                server_hostname=cn) as conn:
            conn.settimeout(timeout)
            conn.connect((cn, port))
            return conn.getpeercert()
    except ssl.CertificateError as e:
        print('Errorr:', e)
        sys.exit(1)


def _conv_dt(datetime_string):
    cert_date_format = '%b %d %H:%M:%S %Y %Z'
    return datetime.datetime.strptime(datetime_string, cert_date_format)


def _get_cert_datetime(cert):
    return _conv_dt(cert['notBefore']), _conv_dt(cert['notAfter'])


def check_issuer(cert):
    if cert.get('issuer'):
        issuer = {key_val[0][0]: key_val[0][1] for key_val in cert.get('issuer')}
    else:
        issuer = {}
    issuer['checkFlag'] = True
    if re.match(r'(Symantec|GeoTrust)', issuer.get('organizationName', '')):
        issuer['checkFlag'] = False
    if re.match(r'RapidSSL', issuer.get('commonName', '')):
        issuer['checkFlag'] = False
    return issuer


def check_cert_datetime(cert):
    start, end = _get_cert_datetime(cert)
    if start < datetime.datetime(2016, 6, 1, 0, 0, 0):
        flag = 1
        msg = 'Cert was published at {}; before 2016-05-31.'
    elif (datetime.datetime(2016, 5, 31, 23, 59, 59) < start and
          start < datetime.datetime(2017, 12, 1, 0, 0, 0)):
        flag = 2
        msg = 'Cert was published at {}; between 2016-06-01 and 2017-11-30.'
    else:
        flag = 0
        msg = 'Cert was published at {}.'
    return flag, msg.format(str(start))


def main(cn):
    try:
        cn, port = cn.strip().split(':')
    except ValueError:
        cn = cn.strip()
        port = 443

    cert = get_cert(cn, int(port))
    issuer = check_issuer(cert)
    cert_datetime = check_cert_datetime(cert)
    cert_start, cert_end = _get_cert_datetime(cert)

    if not issuer['checkFlag']:
        if (cert_datetime[0] == 1 and
                cert_end > datetime.datetime(2016, 6, 1, 0, 0, 0)):
            state_msg = 'CRITICAL-1'
        elif (cert_datetime[0] == 2 and
              cert_end > datetime.datetime(2018, 9, 12, 23, 59, 59)):
            state_msg = 'CRITICAL-2'
        else:
            state_msg = 'WARNING'
    else:
        state_msg = 'OK'

    message = '[{: ^12}] {}: {} ({}, {})'.format(
        state_msg, cn, cert_datetime[1], issuer.get('organizationName', '-'),
        issuer.get('commonName', '-')
    )
    return message


if __name__ == '__main__':
    print(main(sys.argv[1]))

参考

WindowsでGitエディタにVS Codeを指定するときのオプション

[core]
    editor = 'C:\\Program Files\\Microsoft VS Code\\Code.exe' -w -n
  • -w, --wwait : VS Codeが閉じられるまで待つ
  • -n, --new-window: 新しいウィンドウで開く

-nも入れてあげないと、既に開いているVS Codeの画面で開かれてしまって、その画面を閉じるまで待たれる...。タブを閉じるのではだめだった...。

Django Class-based views でCSVダウンロードページの実装ではまったこと

公式ドキュメント Outputting CSV with Django | Django documentation | Django とか、GoDjangoの記事 Download CSV files via CSVResponseMixin - GoDjango とかあるからさくっと行けるだろうと思ったら、意外な落とし穴があった。

ダウンロードされるCSVファイルのファイル名をレスポンスヘッダーの"Content-Disposition"に入れてあげるのですが、Chromeさんだと特に何も考えず、

GoDjangoにある以下の書き方で、日本語名でも全く問題なく行ってくれました。

response = HttpResponse(content_type='text/csv')
cd = 'attachment; filename="{0}"'.format(self.get_csv_filename())
response['Content-Disposition'] = cd

が残念ながら、Firefoxさんだとファイル名を受け取ってくれません。 Google先生に問い合わせると、先人がいらっしゃいました(PHPでの実装だけど)。

scientre.hateblo.jp

比較的最近のブラウザでは RFC 2231 形式に対応しているので、通常はこの形式を使えば問題は少ないだろう。

ということなので、 RFC 2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations を読んだつもりになって、「python rfc2231」とかってググると、 19.1.14. email.utils: 多方面のユーティリティ — Python 3.6.5 ドキュメント 使っとけって出てくるので、使ってみたら無事解決。

たぶん使いまわしがきくであろうMixiにしておいてみた。

UTF-8CSVをうまくExcelさんに読んでもらうためには、BOM付UTF-8にしないといけないらしく、それでも微妙にはまった(というのも↑Mixinは対応)。エンコーディング問題というのはどこへ行っても頭を悩ませる種ですね。。。