Refactoring C CodeStarting with an API
In my last post, I introduced a basic error handling mechanism for C API. The code I showed had a small memory leak in it. I love that fact about it, because it is hidden away and probably won’t show up very easily for production code until very late in the game. Here is the fix:
The problem was that I was freeing the error struct, but wasn’t freeing the actual error message. That is something that is very easy to miss, unfortunately. Especially as we assume that errors are rare. With that minor issue out of the way, let’s look at how we actually write the code. Here is the updated version of creating a connection, with proper error handling:
The calling code will check if it got NULL and then can decide whatever it should add it’s own error (for context) and return a failure to its own caller or handle it. The cleanup stuff is still annoying with goto, but I don’t believe that there is much that can be done about it.
I’m going to refactored things a bit, so I have proper separation and an explicit API. Here is what the API looks like now:
This is basically a wrapper around all the things we need to around SSL. It handles one time initialization and any other necessary work that is required. Note that I don’t actually expose any state outside, instead just forward declaring the state struct and leaving it at that. In addition to the overall server state, there is also a single connection state, whose API looks like this:
Now, let’s bring it all together and see how it works.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
int main(int argc, char **argv) { int rc; int sock = -1; struct server_state* srv_state; const char* cert = "cert.pem"; const char* key = "key.pem" ; sock = create_server_socket(4433); if (sock == -1) { goto handle_error; } srv_state = server_state_create(cert, key); if (srv_state == NULL) { goto handle_error; } while (1) { struct sockaddr_in addr; struct connection* con; unsigned int len = sizeof(addr); char buffer[256]; int client = accept(sock, (struct sockaddr*)&addr, &len); if (client == INVALID_SOCKET) { push_error(ENETRESET, "Unable to accept connection, error: %i", GetLastError()); // failure to accept impacts everything, we'll abort goto handle_error; } con = connection_create(srv_state, client); if (con == NULL) { closesocket(client); print_all_errors(); continue; // accept the next connection... } if (connection_write_format(con, "Connected!\n") == 0) { goto handle_connection_error; } while (1) { // now, let's do echo server... rc = connection_read(con, buffer, sizeof buffer); if (rc == 0) { goto handle_connection_error; } if (connection_write(con, buffer, rc) == 0) { goto handle_connection_error; } } handle_connection_error: print_all_errors(); if (con != NULL) { connection_drop(con); } // now go back and accept another connection } handle_error: if(srv_state != NULL) server_state_drop(srv_state); if (sock != -1) closesocket(sock); print_all_errors(); return EINVAL; }
This is a pretty trivial echo server, I’ll admit, but it actually handles things like SSL setup, good error handling and give us a good baseline to start from.
The code for this series can be found here and this post’s code is this commit. Next, I want to see what it would take to setup client authentication with OpenSSL.
More posts in "Refactoring C Code" series:
- (12 Dec 2018) Going to async I/O
- (10 Dec 2018) Do we need a security review?
- (07 Dec 2018) Implementing parsing
- (04 Dec 2018) Multi platform and Valgrind
- (03 Dec 2018) Choosing the next direction
- (30 Nov 2018) Giving good SSL errors to your client…
- (29 Nov 2018) Starting with an API
- (27 Nov 2018) Error handling is HARD, error REPORTING is much harder
Comments
Comment preview