ユーニックス総合研究所

  • home
  • archives
  • python-https-socket

PythonのsocketでHTTPSクライアントを作る

  • 作成日: 2020-12-08
  • 更新日: 2023-12-24
  • カテゴリ: Python

socketでHTTPSクライアントを作る

最近のWebサイトはみんなHTTPSになってますね。
Pythonのsocketモジュールを使うとソケット通信を行うプログラムを作ることが出来ます。

このソケット通信でHTTPSのページのコンテンツを取得するプログラムを今回は作ってみようと思います。

HTTPとHTTPSではソケット通信のコードの一部を変更する必要がありますが、今回解説するのはHTTPSです。
WebページにHTTP(HTTPS)のリクエストを送信して、そのページのコンテンツを取得し、画面に表示するという具合です。

具体的には↓を見ていきます。

  • HTTPの概要
  • ソケットをSSLに対応させる方法
  • HTTP(HTTPS)クライアントの作成

HTTPの概要

ホストとホストの間の通信で使われる約束事を「プロトコル」と言います。
WebサーバーとWebクライアントのあいだで使われるのは「HTTP」というプロトコルです。

HTTPとはまずクライアントがサーバーに「HTTPリクエスト」を送信します。
HTTPリクエストには、サーバーの「どこの場所のコンテンツ」に対して「どんなメソッドを使うか」などが書かれています。

サーバーはクライアントが送信したHTTPリクエストを受信し、解析します。
そしてメソッドを判断し、指定された場所に対してそのメソッドの処理を適用します。
たとえばメソッドが「GET」で場所が「/hige」であれば「/higeのコンテンツを取得する」という具合です。

サーバーは指定されたメソッドの実行が完了したら、その結果を「HTTPレスポンス」としてクライアントに返します。
この時クライアントはサーバーからデータを読み込んでいる状態です。

つまり通信の順番的には↓のようになっているわけです。

  • クライアントがサーバーにHTTPリクエストを送信する
  • サーバーがクライアントからHTTPリクエストを読み込む
  • サーバーがHTTPリクエストを解析し、メソッドを実行する
  • サーバーがクライアントへHTTPレスポンスを送信する
  • クライアントがサーバーからHTTPレスポンスを読み込む

このようなやり取りを経て、サーバーとクライアントはリクエストとレスポンスを交換します。
クライアントがWebブラウザであれば、レスポンスを解析して画面にグラフィカルな表示を行います。

SSLを使ったソケットの通信でも↑の基本的なやり取りは一緒です。
SSLの関連の処理はPythonのsslというモジュールが隠蔽してくれて、開発者は特に意識する必要もありません。
気をつけたいのはHTTPのデフォルトポートが80であるのに対し、HTTPSは443であるというところです。

ソケットをSSLに対応させる方法

ソケットの作成はPythonのsocketモジュールを使います。
このソケットではデフォルトではSSLに対応していません。
そのためこのソケットをSSLでラップする必要があります。

Pythonのsslというモジュールを使うとsocketで作成したソケットをSSLでラップすることが出来ます。
つまりHTTPSなWebサイトとソケット通信を行いたい場合は、↓のモジュールが必要になるわけです。

  • socket
  • ssl

sslモジュールを使うと非常に簡単にSSLを利用したソケットを作成することが出来ます。

HTTP(HTTPS)クライアントの作成

ではHTTP(HTTPS)クライアントを作成します。
↓がコード全文です。

import socket  
import ssl  


HOST = 'this.is.my.host.com'  # 通信先のホスト(適当に変えてください)  
PORT = 443  # 通信に使うポート  


def get_root(ssock):  
    """  
    HOSTの/からHTTPを使用してコンテンツを読み込む  
    """  
    # HTTPリクエスト・ヘッダーを作成  
    msg = b'GET / HTTP/1.1\r\n'  # /のコンテンツを取得する  
    msg += b'Host: ' + HOST.encode() + b'\r\n'  # ホストの指定  
    msg += b'Connection: close\r\n'  # 接続後に閉じる  
    msg += b'\r\n'  # <- この改行がおわりの合図  

    # サーバーにリクエストを送信  
    ssock.send(msg)  

    # サーバーからレスポンスを読み込み  
    chunks = []  # チャンクのリスト  
    while True:  
        chunk = ssock.recv(1024)  # 1024バイトずつ読み込む  
        if chunk == b'':  # 結果が空ならおわり  
            break  
        chunks.append(chunk)  # 読み取ったチャンクを追加  

    # チャンクのリストを文字列に変換  
    s = ''  
    for chunk in chunks:  
        # UTF-8固定でバイト列を文字列に変換  
        # 'ignore'でエラーを無視する  
        s += chunk.decode('utf-8', 'ignore')  

    # OK  
    return s  


def main():  
    # SSLコンテキストを作成  
    context = ssl.create_default_context()  

    # 普通のソケットを作成  
    with socket.create_connection((HOST, PORT)) as sock:  
        # コンテキストでソケットをラップする  
        with context.wrap_socket(sock, server_hostname=HOST) as ssock:  
            # /のコンテンツを取得する  
            result = get_root(ssock)  

    # 結果を出力  
    print(result)  


# main関数の呼び出し  
main()  

順を追って↓で解説していきます。

main関数内の処理

最初に実行されるのはmain関数で、↓のようにコードの最後の方で呼び出しています。

# main関数の呼び出し  
main()  

main関数の中では↓のように最初にSSLのコンテキストを作成します。

    # SSLコンテキストを作成  
    context = ssl.create_default_context()  

このコンテキストを使うことでソケットをラップすることが出来ます。
次に↓のようにソケット(sock)を作成します。

    # 普通のソケットを作成  
    with socket.create_connection((HOST, PORT)) as sock:  
        ...  

socket.create_connection()の引数にはタプルを渡します。
タプルの第1要素には接続先のアドレス、第2要素にはポート番号を渡します。
これらのHOSTPORTはコードの先頭の方に↓のように書いてあります。

HOST = 'this.is.my.host.com'  # 通信先のホスト(適当に変えてください)  
PORT = 443  # 通信に使うポート  

↑のようにHOSTには文字列で通信先のホストのアドレスを指定します。
これはたとえばURLが「https://this.is.my.host.com/hige」なら「this.is.my.host.com」の部分がHOSTです。
ポート番号にはHTTPSのデフォルトポート番号である443を指定します。

次に作成したソケット(sock)を↓のようにSSLでラップします。

        # コンテキストでソケットをラップする  
        with context.wrap_socket(sock, server_hostname=HOST) as ssock:  
            ...  

↑のコードで作成されたssockはSSLでラップされたソケットです。
このソケットを使うことでリクエストの送信とレスポンスの受信を行うことが出来ます。

次にラップしたソケット(ssock)を使ってWebページのコンテンツを取得します。
コンテンツの取得は↓のようにget_root()という関数で行います。

            # /のコンテンツを取得する  
            result = get_root(ssock)  

↑のようにget_root()ssockを引数に取り、結果を文字列で返します。
つまり↑のresultにはWebページのコンテンツが代入されます。

get_root関数の処理

get_root()の処理を見てみます。

HTTPリクエストの作成

get_root()では↓のように最初にHTTPリクエストを作成します。

    # HTTPリクエスト・ヘッダーを作成  
    msg = b'GET / HTTP/1.1\r\n'  # /のコンテンツを取得する  
    msg += b'Host: ' + HOST.encode() + b'\r\n'  # ホストの指定  
    msg += b'Connection: close\r\n'  # 接続後に閉じる  
    msg += b'\r\n'  # <- この改行がおわりの合図  

ソケット通信でやり取りされるデータはバイト列です。
そのためHTTPリクエストも↑のようにバイト列で作成します(b'text'などがバイト列です)。

HTTPリクエストのフォーマットで気をつけたいのは、リクエスト内の改行はCRLFで書くことです。
↑の場合\r\nCRLFの改行です。

リクエストの最初の行に↓のようにメソッドとメソッドを適用させる場所、それからHTTPのバージョンを書きます。
GETがメソッドで、/が場所、HTTP/1.1がHTTPのバージョンです。
GETは「取得」を表すメソッドです。つまり「/のコンテンツを取得(GET)したい」というリクエストになります。
プロトコルにもバージョンがあるので↓のように明示的に指定します。

    msg = b'GET / HTTP/1.1\r\n'  # /のコンテンツを取得する  

↓のようにHost:の行には通信先のホスト名を書きます。これはHOST変数の値を流用しています。

    msg += b'Host: ' + HOST.encode() + b'\r\n'  # ホストの指定  

↓のようにConnection:の行には現在のトランザクションが完了した後にネットワーク接続を継続するか指定します。
closeを指定するとトランザクションが完了したとに接続が閉じます。
これを指定しないとsocket.recv()で延々に受信することになるので注意が必要です。

    msg += b'Connection: close\r\n'  # 接続後に閉じる  

↓のようにHTTPリクエストのヘッダーの最後に改行を入れておきます。
この改行がヘッダーの終端です。つまりこの改行のあとはリクエストのボディ部分になり、POSTのデータなどはここにデータが書かれます。

    msg += b'\r\n'  # <- この改行がヘッダー部分のおわりの合図  

サーバーにHTTPリクエストを送信

HTTPリクエストを作成したらソケットのsend()メソッドを使ってサーバーにリクエストを送信します。

    # サーバーにリクエストを送信  
    ssock.send(msg)  

サーバーからレスポンスを読み込む

リクエストを送信したら次にサーバーからレスポンスを読み込みます。
読み込みにはソケットのrecv()メソッドを使います。
このメソッドの第1引数には読み込みの最大バイト数を指定します。

読み込んだデータはchunkとしてchunksリストに追加していきます。
recv()が空のバイト列を返して来たら読み込むデータが無いのでループからブレークします。
こうすることでchunksリストに読み込んだバイト列が格納されていきます。

    # サーバーからレスポンスを読み込み  
    chunks = []  # チャンクのリスト  
    while True:  
        chunk = ssock.recv(1024)  # 1024バイトずつ読み込む  
        if chunk == b'':  # 結果が空ならおわり  
            break  
        chunks.append(chunk)  # 読み取ったチャンクを追加  

chunksを文字列に変換

chunksはリストで、中身はバイト列です。
これを文字列に変換します。
バイト列のメソッドdecode()を使うと文字列に変換できます。
エンコーディングはutf-8を指定し、エラーが発生したら無視するようにignoreを指定します。
utf-8固定になっているのでsjisのページを読み込みたい場合はここのエンコーディングを変更してください。

    # チャンクのリストを文字列に変換  
    s = ''  
    for chunk in chunks:  
        # UTF-8固定でバイト列を文字列に変換  
        # 'ignore'でエラーを無視する  
        s += chunk.decode('utf-8', 'ignore')  

最後に変換した文字列をreturnします。

    # OK  
    return s  

これでget_root()はおわりです。

実行してみよう

このスクリプトを保存しPythonで実行すると、HOSTのサーバーに対してHTTP通信を行います。
そしてルート(/)のページのコンテンツが画面に出力されます。
このようにsocketsslモジュールを使うと比較的に簡単にHTTPSのページを取得することが可能です。

🦝 < socketとsslの戯れ