Pythonのsocketとsslでサーバを作りブラウザからアクセスさせてみた

HTTPSクライアントの動作確認を行う時などで、ダメなHTTPヘッダとか文法的にNGなHTMLあるいは文法的にNGなJavaScriptを相手に送り返したいというのが目的で作りました。

* sslのwrap_socketを使っています
* Web系のフレームワークとかではないので、ヘッダとかボディはまぁ好き勝手にやれます。

環境としては以下の感じです。

* 確認はオレオレ認証でやりました
* Ubuntu 18-04、Python 3.7.3 で動作を確認しました。
* Windows10のFireFoxとChromeで動作確認しています。あとSafari系も多分動くとは思います。

## 証明書関係

opensslを使ってやります。

オレオレでとにかくSSLしてくれさえすりゃいいってんでしたら、以下にやり方が書いてあります（参考にさせて頂きました。ありがとうございます）

https://qiita.com/masakielastic/items/05cd6a36bb6fb10fccf6

https://stackoverflow.com/a/41366949/531320

例えば鍵を mock.key 、証明書を mock.crt というファイル名にしたいなら以下です。ドメインは超適当（使わないので…）

```sh
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout mock.key -out mock.crt -subj "/CN=test.com" -days 3650
```

これで後Python書けばSSL出来ます。

## Pythonコード

以下の記事のコードをベースに書かせて頂きました（参考にさせて頂きました。ありがとうございます）

https://qiita.com/butada/items/9450e39d8d4aac6ac1fe

公式のドキュメントは以下。

https://docs.python.org/3/library/ssl.html


で、書いたコードがこれっす。

```py

#!/usr/bin/env python
# coding: utf-8

import socket
import ssl
import sys

'''
ここらは環境とかに応じてよしなにお願いします
'''
TEST_IP = '10.0.0.12'
TEST_PORT = 443
TEST_KEY = "mock.key"
TEST_CERT = "mock.crt"


class TestSSLServer:

    '''
    SSLサーバの基本動作系を雑にまとめてみた
    '''

    def init_soccket(self, key, cert, ip, port):
        self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        self.ssl_context.load_cert_chain(certfile=cert, keyfile=key)

        self.bind_socket = socket.socket()
        self.bind_socket.setsockopt(
            socket.SOL_SOCKET,
            socket.SO_REUSEADDR,
            1)
        self.bind_socket.setsockopt(
            socket.SOL_SOCKET,
            socket.SO_REUSEPORT,
            1)
        self.bind_socket.bind((ip, port))
        self.bind_socket.listen(5)  # 適当っす

    def finsih_socket(self):
        self.connection_stream.shutdown(socket.SHUT_RDWR)
        self.connection_stream.close()

    def accept_socket(self):
        while True:
            try:
                self.accept_socket, _fromaddr = self.bind_socket.accept()
                self.connection_stream = self.ssl_context.wrap_socket(
                    self.accept_socket, server_side=True)
                return
            except ssl.SSLError:
                '''
                Chrome, Firefoxはこっちのエラーが出る
                オレオレでもいいから接続に備えてacceptに戻る
                '''
                print("wrap: SSL error:", sys.exc_info()[0])
                continue
            except OSError:
                '''
                Safari系はこっちのエラーが出る
                オレオレでもいいから接続に備えてacceptに戻る
                '''
                print("wrap: OS error:", sys.exc_info()[0])
                continue
            except BaseException:
                '''
                他は知らないので、プロセス終了して無かった事にしますｗ（CTRL-Cとかがここに来るはず）
                '''
                print(
                    "wrap: Unexpected error:",
                    sys.exc_info()[0])
                sys.exit(1) 

    def read_stream(self):
        bin_data = self.connection_stream.read()
        return bin_data

    def write_stream(self, bin_data):
        self.connection_stream.sendall(bin_data)


if __name__ == '__main__':

    '''
    初期化
    '''
    server = TestSSLServer()
    server.init_soccket(TEST_KEY, TEST_CERT, TEST_IP, TEST_PORT)

    '''
    接続待ち
    '''
    server.accept_socket()

    '''
    リクエスト、レスポンス一回ずつ
    '''
    bin_data = server.read_stream()
    if bin_data:
        print (bin_data)
        content = "<html>test</html>"
        text = "HTTP/1.1 200 OK\r\n"
        text += "Content-Length: " + str(len(content)) + "\r\n\r\n"
        text += content
        server.write_stream(text.encode())

    '''
    切断、リソース解除
    '''
    server.finsih_socket()

```

元のコードはPython2でしたが3で動くようにしています。また途中で止めた時とかの事も考慮して、SO_REUSEADDRの設定もしています。

後、ブラウザからアクセスしてもOKなようにエラー時の挙動を変更しています。具体的にはオレオレに起因して認証に失敗しますと、ssl.SSLErrorとかOSErrorが出ますので、その際にはacceptからやりなおすだけっすｗ

* TEST_IP, TEST_PORT, TEST_KEY, TEST_CERTはそれぞれ自分の環境に合わせて変更下さい。
* 何がしかのリクエストが来たら、レスポンスを返して終わりにするだけの簡単なコードとなっています。受けたレスポンスは確認用にprintしています。
* 想定しない問題が起きたらsys.exitでプロセス事無かった事にする適当なプログラムですのでそこはご容赦。
* なので接続前にCTRL-Cとかでも抜ける事が出来るはず。

コードを見ると分かりますがヘッダとかボディも自分で全部書かないとダメです。つまり、文法とかも気にせず好きなデータを突き返す事が出来ます。

## 実行方法

実行方法ですがポート443使うとなるとUbuntuではsudoする必要がありました。こんな感じですね。

```sh
sudo python3.7 sample_server.py
```

後はブラウザから 

```sh
https://xxx.xxxx.xxxx.xxx:nnnn 
```
って感じでアクセスすればいいです。TEST_PORTを443にしてあれば:nnnnは不要です。オレオレなんで、証明書に関してお叱りを受けるかと思いますが、承知の上で接続という流れでやると test と出てくる筈です。