PythonのSSLHandshakeErrorにはまる

AWS LambdaとAPI Gatewayに作成したAPIを、Pythonから呼び出す際にはまったので覚え書として残す。

動作環境

クライアント

Pythonで下記のクライアントを作成した。事前にcurlを使って動作確認は出来ていたので、サーバ側のAPI処理は問題ない。

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

myheaders = {
  "x-api-key" : "xxx",
  "Content-type" : "application/json",
  "Accept" : "application/json"
}
mybody = {
  "value" : "123"
}
mybody = json.dumps(mybody)
 
url = "https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod/xxx"
ht = httplib2.Http()
res, content = ht.request(url, "POST", mybody, headers = myheaders)

print res.status
print content

問題

ところが実際に動かしてみると、下記のSSLエラーが発生してしまう。

$ ./post_data_error.py 
Traceback (most recent call last):
  File "./post_data_error.py", line 21, in <module>
    res, content = ht.request(url, "POST", mybody, headers = myheaders)
  File "/usr/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1609, in request
    (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
  File "/usr/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1351, in _request
    (response, content) = self._conn_request(conn, request_uri, method, body, headers)
  File "/usr/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1272, in _conn_request
    conn.connect()
  File "/usr/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1059, in connect
    raise SSLHandshakeError(e)
httplib2.SSLHandshakeError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:590)

SSLHandshakeErrorとは一体何だろう?SSLが絡むエラーなので(良く有る様に)証明書が不足しているのかと思って調べてみたが、そうでは無いらしい。調べた限りでは、SSLのコネクション時に必要な実装が含まれていないらしいと分かった。

The problem is the lack of the Server Name Indication (SNI) extension in the TLS handshake, which the twitrss.me site apparently requires:
...
I also looked at packet dumps to verify that SNI was missing when attempting connection using Python.

python - SSLv3 alert handshake failure with urllib2 - Stack Overflow

対策

考えてみれば、たまたま手元に有ったPythonスクリプトを流用して作ったクライアントなので、httplib2を使う必然性は実は全くない。実際、httplib2ではなくurllib2を使う形に書き換えたら(変更箇所は少なく容易)、urllib2でも上記のエラーが発生することなく正常に処理が完了した。

import urllib
import urllib2
...
req = urllib2.Request(url, headers=myheaders)
req.add_data(mybody)
res = urllib2.urlopen(req)
print res.code,
print res.read()

httplib2とurllib/urllib2は、使い勝手に差が有る程度と認識していたのだが、実は内部実装の違いに起因する機能面での差異もあると初めて知った。

urllib/urllib2 is built on top of httplib. It offers more features than writing to httplib directly.
however, httplib gives you finer control over the underlying connections.

http - Python urllib vs httplib? - Stack Overflow

結論

もっとも、時代は更に先を進んでいて、今はurllib2ではなく、Requestsを使うのがオススメらしい。(ちなみに"NON-GMO"とは「遺伝子組み換え食品を使っていない」という意味なので、不純物が含まれておらず開発者に優しいとのこと...)

より高いレベルの http クライアントインターフェイスとしては、 Requests package がお奨めです。

20.6. urllib2 — URL を開くための拡張可能なライブラリ — Python 2.7.14 ドキュメント

Requests is the only Non-GMO HTTP library for Python, safe for human consumption

Requests: HTTP for Humans™ — Requests 2.21.0 documentation

結局、今回のスクリプトは下記の様に書き換えておいた。(こちらも変更箇所は少なく移行は容易。もちろんSSLエラーは発生しない)

import requests
...
res = requests.post(url, data=mybody, headers=myheaders)
print res.status_code
print res.json()

受信したデータをjsonにまでデコードしてくれるのは、なるほど今時のライブラリだと思う。