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:
python - SSLv3 alert handshake failure with urllib2 - Stack Overflow
...
I also looked at packet dumps to verify that SNI was missing when attempting connection using Python.
対策
考えてみれば、たまたま手元に有った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.
http - Python urllib vs httplib? - Stack Overflow
however, httplib gives you finer control over the underlying connections.
結論
もっとも、時代は更に先を進んでいて、今は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にまでデコードしてくれるのは、なるほど今時のライブラリだと思う。