There was a problem: there is a Windows application that makes HTTPS requests to the server and receives responses. After updating the server, the application stopped working. It turned out that the SSL version was changed on the server (switched from SSLv3 to TLSv1), and our application can work only over SSLv3. Nobody supports the application for a long time and did not want to change, recompile, test. It was decided to make a layer between the application and the server, which will translate SSLv3 to TLSv1 and vice versa. I looked for some proxy on the Internet, but I didn’t find it right away (I was looking badly). I decided to make a proxy on python. I am not a professional in python, but it seemed to me that it is well suited for this task, and it is interesting in parallel to study python using the example of a real problem.
StartSo, set the python 3.4. We write a script, I used a notepad for this. For ssl sockets, you need an ssl module. For, in fact, sockets socket.
import ssl import socket
We create the socket listening to the client since this will be an SSL server, you will have to create a self-signed certificate for it, which it will provide to the client. To create a certificate, I used the openssl utility. I downloaded the utility from here
indy.fulgan.com/SSL . To create a certificate, you will need a config for the utility, an example here is
web.mit.edu/crypto/openssl.cnf . Put the config in a folder on the computer and set the path to it (hereinafter all the actions on the command line):
set OPENSSL_CONF=__\openssl.cnf
Generate private key
openssl genrsa -des3 -out server.key 1024
Along the way, it will be proposed to enter the password to the key and confirm the password, enter. Create a certificate request
openssl req -new -key server.key -out server.csr
When generating a request, we will need to enter the key password and fill in information about the company, city, country, etc. Fill. In order to use the key without a password, copy it and unload it.
copy server.key server.key.org openssl rsa -in server.key.org -out server.key
Finally, create a self-signed certificate.
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
For convenience, we put our certificate and key next to the python script. We create a socket that will listen to the client and set it to listen to the port on which our application will go (hereinafter code in python)
sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True) sock.bind( ('localhost', 43433) ) sock.listen(10)
We receive incoming connection and request from the client
conn, addr = sock.accept() data = conn.recv(1024)
Next, we need to send the received data to the server to which they were intended. We create for this a socket and a helmet data in it.
serv = ssl.wrap_socket(socket.socket()) serv.connect( ('server_url', 443) ) serv.send(data)
So, the request was sent, now we need to get answers and give it to our client
data = serv.recv(1024) conn.send(data)
Well, all our proxy is ready, we launch, we throw the request - it does not work! To find out why, let's add logging.
Logging')
We will connect the logging module, configure the logging configuration and add logging to interesting places.
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)
Read all dataIt turned out that the client transmits the data in blocks, i.e. we did not read the full request. Then it turns out that the server also gives a block response. We will improve our code to read the request and the answer by blocks. To do this, we create a buffer in which we will add the entire request, set the socket a timeout of 0.1 seconds, which it will wait for the data from the incoming connection and read in the loop and add the data to the buffer. If there is no data, we will get an exception and exit the loop.
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)
The same for reading data from the server
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)
Change the data that will be sent to the server and client
logging.info(" ") serv.send(req) logging.info(" ") client.send(resp)
We start. Now it works, but you have to run the script with each request to the server, which is not very convenient.
Handling multiple requestsWe will improve the script, after processing the request, we will again listen to the socket
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)
This will work, but there is a problem - we have an endless loop from which the program cannot exit normally. To exit, you can use the keyboard interrupt Ctrl + C and send a request, after which the program ends with the exception KeyboardInterrupt.
Service stopIn order to provide a more or less normal output, I decided to send a STOP to the socket, this will be the final control command. Let's write a handler for such a command. To do this, we need to modify the code reading from the client socket. We get the first four bytes and if they are STOP, we interrupt the cycle.
logging.info(" ") data = conn.recv(4) if data == b'STOP': break
Let's write a function to stop our proxy. We will create a socket (ssl) in it and send STOP to our proxy
def stop(): logging.info(""); me = ssl.wrap_socket(socket.socket()) me.connect( ('localhost', 43433) ) me.send(b'STOP') me.close()
To run the STOP command, we will use the command line parameter. If you passed the stop line on the command line, then we will call our stop () function (We place this code and the stop function at the beginning, after setting the logging format).
if len(sys.argv) > 1: if sys.argv[1] == "stop": stop();
Now we can stop our proxy with the same script. To stop the server startup code from running after the stop, wrap the main code in the run function, we’ll get
def run():
At the same time processed the case with the wrong command.
DemonizationThere is a problem, when launching our proxy, the application will hang on the command line, at first glance it seems that it is hanging. To solve this problem let's make a demon. Since we have Windows, then the daemon is done here by running the process without a window, this code will be non-platform. So, let's write the daemonize () function.
import subprocess def daemonize(): logging.info(" "); subprocess.Popen("py proxy.py", creationflags=0x08000000, close_fds=True)
Here creationflags = 0x08000000, setting the CREATE_NO_WINDOW flag for the process. We will start our service in daemon mode if we send start on the command line
if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); elif sys.argv[1] == "start": daemonize(); else: print(" ", sys.argv[1]) else: run()
Now we can start our service in daemon mode and stop.
MultitaskingAnother small touch, we add the ability to handle multiple clients, for this we take out our code of work with the client in a separate function
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)
And in the main function, we will run client_run in a separate thread, since we installed socket.listen (10), then at the same time we can have up to 10 threads
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("")
Now our proxy service is ready.
PS: Later, my colleague suggested to me that for my task you can use stunnel, and I decided to put it, and put the script here, all of a sudden it will be interesting to anyone. Config for stunnel this:
[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
With stunnel also had to tinker, because there were incorrect settings on the server and SNI verification did not pass, it only worked with version 4.36, since there is no such verification.
Sources on github
github.com/sesk/py_proxy