PythonでSSLトンネルを作成する

問題が発生しました。サーバーへのHTTPS要求を作成し、応答を受信するWindows用のアプリケーションがあります。 サーバーの更新後、アプリケーションは動作を停止しました。 サーバー上のSSLのバージョンが変更され(SSLv3からTLSv1に切り替えられ)、アプリケーションがSSLv3でのみ動作することが判明しました。 誰も長い間アプリケーションをサポートしておらず、変更、再コンパイル、テストを望んでいませんでした。 SSLv3をTLSv1に、またはその逆に変換するアプリケーションとサーバーの間にレイヤーを作成することが決定されました。 インターネットでプロキシを検索しましたが、すぐには見つかりませんでした(検索が不十分でした)。 pythonでプロキシを作成することにしました。 私はpythonの専門家ではありませんが、このタスクに適していると思われ、実際の問題の例を使用してpythonを並行して研究することは興味深いです。



開始する



したがって、Python 3.4をインストールします。 スクリプトを書いています。これにはメモ帳を使用しました。 sslソケットの場合、sslモジュールが必要です。 実際には、ソケットソケット用です。

import ssl import socket
      
      





クライアントをリッスンするソケットを作成します。 SSLサーバーの場合、そのサーバー用の自己署名証明書を作成する必要があります。この証明書はクライアントに提供されます。 証明書を作成するために、opensslユーティリティを使用しました。 ここからindy.fulgan.com/SSLからユーティリティをダウンロードしました 。 証明書を作成するには、ユーティリティの設定が必要です。例はweb.mit.edu/crypto/openssl.cnfにあります。 コンピューター上のフォルダーに構成を配置し、そのパスを設定します(コマンドラインのすべてのアクション)。

 set OPENSSL_CONF=__\openssl.cnf
      
      





秘密鍵を生成します

 openssl genrsa -des3 -out server.key 1024
      
      





途中で、キーとパスワードの確認のためにパスワードを入力するように求められます。 証明書リクエストを作成する

 openssl req -new -key server.key -out server.csr
      
      





リクエストを生成するとき、キーパスワードを入力し、会社、都市、国などに関する情報を入力する必要があります。 記入します。 パスワードなしでキーを使用できるようにするには、キーをコピーして仮釈放します

 copy server.key server.key.org openssl rsa -in server.key.org -out server.key
      
      





最後に、自己署名証明書を作成します

 openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
      
      





便宜上、Pythonスクリプトの横に証明書とキーを配置します。 クライアントをリッスンするソケットを作成し、アプリケーションが進むポート(以降、Pythonのコード)をリッスンするように設定します

 sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True) sock.bind( ('localhost', 43433) ) sock.listen(10)
      
      





クライアントから着信接続とリクエストを受け取ります

 conn, addr = sock.accept() data = conn.recv(1024)
      
      





次に、受信したデータを目的のサーバーに送信する必要があります。 このためにソケットとヘルメットのデータを作成します

 serv = ssl.wrap_socket(socket.socket()) serv.connect( ('server_url', 443) ) serv.send(data)
      
      





リクエストが送信されたので、今度はレスポンスを取得してクライアントに渡す必要があります

 data = serv.recv(1024) conn.send(data)
      
      





さて、すべてのプロキシの準備ができて実行され、リクエストを投げる-それは動作しません! 理由を調べるには、ログを追加します。



ロギング



ロギングモジュールを接続し、ロギングコンフィグレーションを構成し、興味深い場所にロギングを追加します

 import logging logging.basicConfig(filename = "proxy.log", level = logging.DEBUG, format = "%(asctime)s - %(message)s") logging.info("  "); conn, addr = sock.accept() logging.info(" ") data = conn.recv(1024) logging.info(data) logging.info("   ") serv.send(data) logging.info("  ") data = serv.recv(1024) logging.info(data) logging.info("  ") client.send(resp)
      
      







すべてのデータを読む



クライアントはブロック単位でデータを送信することがわかりました。 完全なリクエストを読んでいません。 その後、サーバーもブロックで応答することがわかりました。 コードを改善して、要求と応答をブロック単位で読み取ります。 これを行うには、要求全体を追加するバッファーを作成し、ソケットを0.1秒のタイムアウトに設定します。これは、着信接続からのデータを待機し、サイクルでデータを読み取ってバッファーに入れます。 データがない場合、例外を取得してループを終了します

 logging.info(" ") data = conn.recv(1024) req = b'' conn.settimeout(0.1) while data: req += data try: data = conn.recv(1024) except socket.error: break logging.info(req)
      
      





サーバーからデータを読み取る場合も同じです

 logging.info("  ") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp)
      
      





サーバーとクライアントに送信するデータを変更します

 logging.info("   ") serv.send(req) logging.info("  ") client.send(resp)
      
      





始めます。 これで動作するようになりましたが、サーバーへのすべてのリクエストでスクリプトを実行する必要があり、あまり便利ではありません。



複数リクエスト処理



スクリプトを改善し、リクエストを処理した後、再びソケットをリッスンします

 while True: logging.info("  "); conn, addr = sock.accept() logging.info(" ") data = conn.recv(1024) req = b'' conn.settimeout(0.1) while data: req += data try: data = conn.recv(1024) except socket.error: break logging.info(req) logging.info("   ") serv.send(req) logging.info("  ") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp) logging.info("  ") client.send(resp)
      
      





これは機能しますが、問題があります-プログラムが通常の方法で終了できない無限ループがあります。 終了するには、キーボード割り込みCtrl + Cを使用してリクエストを送信します。その後、KeyboardInterruptを除き、プログラムは終了します。



サービス停止



多かれ少なかれ通常の出力を提供するために、STOPをソケットに転送することにしました。これが終了の制御コマンドになります。 このようなコマンドのハンドラーを作成します。 これを行うには、クライアントソケットからの読み取りコードを変更する必要があります。 最初の4バイトを受け取り、それらがSTOPの場合、サイクルを中断します。

  logging.info(" ") data = conn.recv(4) if data == b'STOP': break
      
      





プロキシを停止する関数を作成します。 その中で、ソケット(ssl)を作成し、プロキシにSTOPを送信します

 def stop(): logging.info(""); me = ssl.wrap_socket(socket.socket()) me.connect( ('localhost', 43433) ) me.send(b'STOP') me.close()
      
      





STOPコマンドを開始するには、コマンドラインパラメーターを使用します。 停止行をコマンドラインに渡した場合、stop()関数を呼び出します(ログ形式を設定した後、このコードと停止関数を最初に配置します)。

 if len(sys.argv) > 1: if sys.argv[1] == "stop": stop();
      
      





これで、同じスクリプトでプロキシを停止できます。 サーバーの起動コードを停止した後に実行されないように、メイン関数をrun関数でラップすると、

 def run(): #    -   def stop(): #    if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); else: print("  ", sys.argv[1]) else: run()
      
      





同時に、間違ったコマンドでケースを処理しました。



悪魔



問題がありました。プロキシアプリケーションを起動するとコマンドラインがハングします。一見するとフリーズしたようです。 この問題を解決するために、デーモンを作成します。 なぜなら Windowsがある場合、ウィンドウなしでプロセスを開始することでデーモンが実行されます。このコードはクロスプラットフォームではありません。 それではdaemonize()関数を書きましょう

 import subprocess def daemonize(): logging.info(" "); subprocess.Popen("py proxy.py", creationflags=0x08000000, close_fds=True)
      
      





ここでcreationflags = 0x08000000、プロセスのフラグCREATE_NO_WINDOWを設定します。 コマンドラインでstartを渡した場合、デーモンモードでサービスを開始します。

 if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); elif sys.argv[1] == "start": daemonize(); else: print("  ", sys.argv[1]) else: run()
      
      





これで、デーモンモードでサービスを開始して停止できます。



マルチタスク



もう1つ小さなタッチとして、複数のクライアントを処理する機能を追加します。これを行うには、クライアント作業コードを別の関数に配置します

 def client_run(client, data): req = b'' logging.info(" ") client.settimeout(0.1) while data: req += data try: data = client.recv(1024) except socket.error: break logging.info(req) serv = ssl.wrap_socket(socket.socket()) serv.connect( ('server_name', 443) ) logging.info("   ") serv.send(req) logging.info("  ") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp) logging.info("  ") client.send(resp)
      
      





そしてメイン関数では、別個のスレッドでclient_runを実行します。 socket.listen(10)をインストールし、同時に最大10個のスレッドを作成できます

 def run(): logging.info("  "); sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True) sock.bind( ('localhost', 43433) ) sock.listen(10) while True: logging.info("  "); conn, addr = sock.accept() data = conn.recv(4) if data == b'STOP': break logging.info(" ") t = threading.Thread(target = client_run, args = ( conn, data ) ) t.run() logging.info("")
      
      





これで、プロキシサービスの準備ができました。



PS:後で、同僚が私の仕事にstunnelを使用できると私に言ったので、誰かが興味があるなら、私はそれを置いて、スクリプトをここに置くことにしました。 stunnelの構成は次のとおりです。

 [client-in] sslVersion = SSLv3 accept = 127.0.0.1:43433 connect = 127.0.0.1:8080 [server-out] sslVersion = TLSv1 client = yes accept = 127.0.0.1:8080 connect = server_name:443
      
      





私はまた、 サーバーの設定が正しくなく、SNI検証に合格しなかったため、バージョン4.36でのみ機能しました。 そのような検証はありません。



githubのソースgithub.com/sesk/py_proxy



All Articles