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

Source: https://habr.com/ru/post/J260085/


All Articles