📜 ⬆️ ⬇️

Cross-platform https server with non-blocking sockets

This article is a continuation of my article The simplest cross-platform server with ssl support .
Therefore, in order to read further it is highly desirable to read at least a part of the previous article. But if you don’t want it, here’s a brief summary: I took the “serv.cpp” file from the OpenSSL sources and made it the simplest cross-platform server that can receive one character from the client.
Now I want to go further and force the server:
1. Accept the entire http header from the browser.
2. Send the browser html page that will display the http header.
3. In addition, I want the sockets not to block the server process and for this I will transfer them to the so-called “non-blocking mode”.

To begin, I will need the serv.cpp file modified in the previous article.
The first thing to do is to write cross-platform macros to put sockets into non-blocking mode:

for this line of code
#ifndef WIN32 #define closesocket close #endif 


change to the following:
 #ifdef WIN32 #define SET_NONBLOCK(socket) \ if (true) \ { \ DWORD dw = true; \ ioctlsocket(socket, FIONBIO, &dw); \ } #else #include <fcntl.h> #define SET_NONBLOCK(socket) \ if (fcntl( socket, F_SETFL, fcntl( socket, F_GETFL, 0 ) | O_NONBLOCK ) < 0) \ printf("error in fcntl errno=%i\n", errno); #define closesocket(socket) close(socket) #endif 

')
Done! Now, to translate the "listening" socket to the non-blocking mode, it suffices immediately after the line
 listen_sd = socket (AF_INET, SOCK_STREAM, 0); CHK_ERR(listen_sd, "socket"); 


insert line:
 SET_NONBLOCK(listen_sd); 


Now the "listening" socket is non-blocking and the accept function will return control to the program immediately after the call.
Instead of a socket handle, accept will now return a value (-1).
Thus, in non-blocking mode, we need to call the accept function in an infinite loop until it returns a socket descriptor.

  int sd = -1; while(sd == -1) { Sleep(1); #ifdef WIN32 sd = accept (listen_sd, (struct sockaddr*) &sa_cli, (int *)&client_len); #else sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len); #endif } 

So that the program does not load the processor at 100%, I added in the Sleep (1) cycle. In Windows, this means a break of 1 millisecond. To make it work in Linux, add at the beginning of the file:

 #ifndef WIN32 #define Sleep(a) usleep(a*1000) #endif 


Theoretically, instead of an infinite loop, you can use the select function and its more powerful counterparts to wait until the listen_sd socket is available for reading, and only then call accept once. But personally, I don’t see any special flaws in my loop method.

So, the program will exit the cycle when the client connects. The sd socket in theory should automatically become non-blocking, but practice shows that for reliability it is better to call the macro at the end of the cycle
 SET_NONBLOCK(sd); 


Now that the socket to communicate with the client is non-blocking, the function
 err = SSL_accept (ssl); 

will not suspend the process, but will return immediately after the call with the value err = SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE
in order to receive an encrypted message, we need another endless loop:

  while(1) { Sleep(1); err = SSL_accept (ssl); const int nCode = SSL_get_error(ssl, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) break; } CHK_SSL(err); 


Only when the program leaves this cycle, you can be sure that the encrypted connection is established and you can begin receiving and sending messages.
We will connect to the server using a browser, so the client's messages consist of the http header and the request body.
At the same time, the http header should end with the string "\ r \ n \ r \ n".
We fix our code so that the server reads the entire http header, and not just its first letter.

In order to shorten the code, I suggest using the excellent STL library:
1. Add three header files:
 #include <vector> #include <string> #include <sstream> 


2. Replace strings

  err = SSL_read (ssl, buf, sizeof(buf) - 1); CHK_SSL(err); buf[err] = '\0'; printf ("Got %d chars:'%s'\n", err, buf); 


on the following code:

  std::vector<unsigned char> vBuffer(4096); //     memset(&vBuffer[0], 0, vBuffer.size()); //   size_t nCurrentPos = 0; while (nCurrentPos < vBuffer.size()-1) { err = SSL_read (ssl, &vBuffer[nCurrentPos], vBuffer.size() - nCurrentPos - 1); //        if (err > 0) { nCurrentPos += err; const std::string strInputString((const char *)&vBuffer[0]); if (strInputString.find("\r\n\r\n") != -1) //   http ,     break; continue; } const int nCode = SSL_get_error(ssl, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) break; } 

In this loop, the server reads data from the client until it receives the end characters of the http header "\ r \ n \ r \ n", or until the buffer space runs out.
It is convenient for me to allocate a buffer as std :: vector, if only because it does not need a separate variable to remember its length.
After exiting the loop, the entire http header and possibly part of the request body should be stored in the buffer.

3. Send the browser html page in which we write the http header of its request.
Replace string
 err = SSL_write (ssl, "I hear you.", strlen("I hear you.")); CHK_SSL(err); 


on the following code:
  //      const std::string strInputString((const char *)&vBuffer[0]); // html     const std::string strHTML = "<html><body><h2>Hello! Your HTTP headers is:</h2><br><pre>" + strInputString.substr(0, strInputString.find("\r\n\r\n")) + "</pre></body></html>"; //    http  std::ostringstream strStream; strStream << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/html; charset=utf-8\r\n" << "Content-Length: " << strHTML.length() << "\r\n" << "\r\n" << strHTML.c_str(); //    . nCurrentPos = 0; while(nCurrentPos < strStream.str().length()) { err = SSL_write (ssl, strStream.str().c_str(), strStream.str().length()); if (err > 0) { nCurrentPos += err; continue; } const int nCode = SSL_get_error(ssl, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) break; } 


Since we have non-blocking sockets, there is no guarantee that the answer will go completely the first time. Therefore, you need to call SSL_write in a loop.
That's all. Now you can start our server and type in the browser localhost:1111
In response, the browser will show the page with its http request.

Project for Visual Studio 2012 in the archive 3_.3s3s.org .
To compile for Linux, copy the files “ca-cert.pem” and “serv.cpp” into one directory and run the compiler: “g ++ -L / usr / lib -lssl -lcrypto serv.cpp”

PS: wrote a continuation of this article

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


All Articles