How to Create an HTTPS Server in C
Build a minimal HTTPS server in C with OpenSSL, serve a camera test page in a secure context, and verify the TLS connection and browser camera access.
The short answer
C has no built-in HTTPS support. To build an HTTPS server in C, you combine ordinary TCP sockets with a TLS library, most commonly OpenSSL. The socket code accepts connections, and the TLS library encrypts them using a certificate and private key. Once the TLS handshake succeeds, you read the HTTP request and write an HTTP response through the TLS layer instead of directly to the socket.
The most common reason to do this for camera or microphone work is the secure context rule. Browsers only expose the camera and microphone API (getUserMedia) to pages they consider secure. A page served over HTTPS qualifies, and so does a page loaded from localhost on the same machine. Plain HTTP from another address, such as your computer's local network IP opened on a phone, generally does not.
Do you actually need HTTPS?
Before writing any TLS code, check which situation you are in, because the answer changes what you need to build.
| Situation | Secure context? | What you need |
| Page opened as localhost on the same computer as the server | Yes, browsers treat localhost as trustworthy | Plain HTTP is usually enough for camera testing |
| Page opened from another device using the server's network IP over HTTP | No | HTTPS with a certificate that device accepts |
| Page opened from another device over HTTPS with a trusted certificate | Yes | The server described in this guide |
| Page opened over HTTPS with an untrusted self-signed certificate | Browser-dependent; expect a warning and possibly unreliable permission behavior | Install a locally trusted certificate on the testing device |
If you only test on one machine, an HTTP server bound to localhost avoids certificates entirely. The rest of this guide covers the case where HTTPS is genuinely required.
What you need
- A C compiler such as gcc or clang
- The OpenSSL development headers and libraries (often packaged as libssl-dev or openssl-devel on Linux; available through Homebrew on macOS)
- A certificate file and its matching private key, both in PEM format (a text-based encoding for keys and certificates)
- A POSIX system such as Linux or macOS for the socket code below; Windows needs Winsock changes
Step 1: Create a certificate for local testing
For a first test, a self-signed certificate is enough to confirm that the server works. This command creates a key and certificate valid for localhost. The subjectAltName option requires OpenSSL 1.1.1 or newer; modern browsers check that field rather than the common name.
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout key.pem -out cert.pem -days 365 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
A self-signed certificate is not trusted by browsers, so you will see a warning. For camera testing from a phone or second computer, a better option is a certificate issued by a local certificate authority that you install on the testing device, or a certificate from a public authority if the server has a real domain name. In either case, the certificate must list the exact name or IP address you type into the browser.
Step 2: Write the server
The program below listens on port 8443, performs the TLS handshake for each connection, and returns a small HTML page. That page reports whether the browser considers it a secure context and then asks for camera access. It handles one connection at a time and ignores the request path, which keeps the example short.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
static const char *BODY =
"<!doctype html><meta charset=utf-8><title>Camera test</title>"
"<p id=s></p><video id=v autoplay playsinline muted width=480></video>"
"<script>"
"const s = document.getElementById('s');"
"s.textContent = 'Secure context: ' + window.isSecureContext;"
"if (navigator.mediaDevices) {"
" navigator.mediaDevices.getUserMedia({video: true})"
" .then(st => { document.getElementById('v').srcObject = st; })"
" .catch(e => { s.textContent += ' | camera error: ' + e.name; });"
"} else { s.textContent += ' | camera API unavailable'; }"
"</script>";
static int create_listener(int port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return -1; }
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); close(fd); return -1;
}
if (listen(fd, 16) < 0) {
perror("listen"); close(fd); return -1;
}
return fd;
}
int main(void) {
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
if (!ctx) { ERR_print_errors_fp(stderr); return 1; }
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
if (SSL_CTX_use_certificate_chain_file(ctx, "cert.pem") <= 0 ||
SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0 ||
!SSL_CTX_check_private_key(ctx)) {
ERR_print_errors_fp(stderr);
return 1;
}
int listener = create_listener(8443);
if (listener < 0) return 1;
printf("Listening on https://localhost:8443/\n");
for (;;) {
int client = accept(listener, NULL, NULL);
if (client < 0) { perror("accept"); continue; }
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
char req[4096];
SSL_read(ssl, req, sizeof(req) - 1); /* request is read but not parsed */
char header[256];
int n = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n\r\n", strlen(BODY));
SSL_write(ssl, header, n);
SSL_write(ssl, BODY, (int)strlen(BODY));
SSL_shutdown(ssl);
}
SSL_free(ssl);
close(client);
}
}
Key parts, in plain terms: SSL_CTX holds the shared TLS settings, including your certificate and key. SSL_accept performs the handshake, which is where the browser checks the certificate and the two sides agree on encryption. After that, SSL_read and SSL_write replace the usual read and write calls. With OpenSSL 1.1.0 and later, library initialization happens automatically, so older tutorials that call SSL_library_init are not needed.
Step 3: Compile and run
gcc server.c -o https_server -lssl -lcrypto
./https_server
Run the program from the folder that contains cert.pem and key.pem. If the compiler cannot find the OpenSSL headers, for example on macOS with a Homebrew installation, add the include and library paths for your OpenSSL install with -I and -L.
Step 4: Verify the result
Check each layer separately. A working TLS connection, a secure context, camera permission and a live video image are four different results, and a failure at one stage points to a different fix.
- TLS works: run curl -k https://localhost:8443/ and confirm the HTML is returned. The -k flag skips certificate verification, so this proves only that encryption and the response work.
- Certificate details: run openssl s_client -connect localhost:8443 and check that the certificate shown is yours and the handshake completes.
- Secure context: open the page in a browser. The first line should read Secure context: true. If it reads false, or says the camera API is unavailable, the browser does not treat the page as secure, usually because of the address used or an untrusted certificate.
- Camera access: the browser should show a permission prompt. Allowing it grants access; it does not yet prove the camera is delivering video.
- Video capture: a moving image in the video element confirms capture. An error name such as NotAllowedError points to a denied permission, while NotFoundError suggests no camera was detected.
Seeing your own video here confirms that the browser can access and capture from the camera. It says nothing about call quality, which also depends on the calling app, network conditions and lighting.
Testing from a phone or another computer
Open the server using your computer's local network address instead of localhost, with port 8443. Three things must line up: the firewall on the server machine must allow incoming connections on that port, the certificate must include that IP address or hostname in its subjectAltName, and the testing device must trust the certificate. If the self-signed certificate from Step 1 only lists localhost and 127.0.0.1, the browser on another device will reject it for the network address.
Limitations of this example
- It serves one client at a time; a slow or stalled connection blocks the others.
- It does not parse the request, so every path, including requests for a site icon, gets the same page.
- It reads the request once and does not handle requests larger than the buffer or split across reads.
- It has no timeouts, logging or protection against malformed input.
- It is meant for local testing, not for exposure on the public internet.
If you need something sturdier, other TLS libraries such as mbedTLS or wolfSSL follow a similar pattern, and an established web server or embedded HTTP library will handle request parsing and concurrency for you. Behavior around self-signed certificates and camera permissions varies between browsers and versions, and no browser-specific test results were available for this guide, so check your browser's own documentation if permission prompts do not appear as expected.