Networking Fundamentals · beginner · ~12 min
- Read an HTTP request and an HTTP response byte by byte, and name each part (request line, status line, headers, blank line, body). - Classify any status code by its first digit (2xx/3xx/4xx/5xx) and predict what a client does with it. - Explain the difference between HTTP (port 80) and HTTPS (HTTP carried over TLS, port 443). - State the three guarantees TLS provides — confidentiality, integrity, and server authentication — and the one it does NOT (availability). - Describe, at a high level, what happens during a TLS handshake and how a certificate chain is validated. - Read a certificate for reconnaissance and robustness value: SAN hostnames, issuer, and validity dates.
The web runs on a beautifully simple idea: a client sends a request made of text, and a server sends back a response made of text. That protocol is called HTTP (HyperText Transfer Protocol). When you type a URL, click a link, or an app calls an API, an HTTP request goes out and an HTTP response comes back.
The catch is that plain HTTP travels in the clear. Anyone on the path — a coffee-shop Wi-Fi router, an ISP, a compromised switch — can read it and change it. HTTPS fixes this. HTTPS is not a different protocol; it is the same HTTP, but sent through an encrypted tunnel built by TLS (Transport Layer Security). Plain HTTP normally uses port 80; HTTPS uses port 443.
This lesson builds directly on TCP vs UDP, ports, and the ones to know. There you learned that TCP gives you a reliable, ordered byte stream and that ports (like 80 and 443) tell the operating system which program should handle an incoming connection. HTTP is just an agreement about what bytes to send over that TCP stream. TLS sits between TCP and HTTP: TCP moves the bytes, TLS encrypts them, and HTTP gives them meaning.
In plain language: HTTP is the language two computers speak; TLS is the sealed envelope that language travels inside. In the terminology you will meet below, HTTP is an application-layer protocol, TLS is a security layer, and HTTPS is simply "HTTP over TLS."
Almost everything you touch online is HTTP or HTTPS. Web pages, mobile-app backends, REST APIs, cloud dashboards, webhooks, software updates, and internal microservices all speak it. If you can read a raw HTTP exchange, you can debug half the problems in modern software without any special tools.
200 OK means "send traffic here"; a 503 means "take it out of rotation." Knowing exactly where that status line lives in the bytes is the difference between a working health-checker and a broken one.Content-Length it never validated, becomes a reliability or security bug when it runs a billion times a day.An HTTP request is text with a strict shape. The very first line is the request line: a method, a path, and a protocol version. After it come headers, one per line, each Name: Value. Then a single blank line marks the end of the headers. Anything after that blank line is the body.
GET /index.html HTTP/1.1 <- request line: METHOD PATH VERSION
Host: example.com <- header
User-Agent: curl/8.0 <- header
Accept: */* <- header
<- BLANK LINE ends the headers
(body goes here, if any)
Each line ends with the two bytes \r\n (carriage return + line feed), and the blank line is an extra \r\n. That \r\n\r\n sequence is how a program knows the headers are finished. Common methods: GET (fetch), POST (send data), PUT, DELETE, HEAD.
When to use which method: GET for reading (no body, safe to repeat), POST for creating or submitting (has a body). Do not put sensitive data in a GET query string — it lands in logs and browser history.
Pitfall: forgetting the Host header. In HTTP/1.1 it is mandatory, because one server (one IP) hosts many sites; the Host header is how the server knows which one you want. Leave it out and you often get a 400 Bad Request.
Knowledge check: In the request above, what exactly separates the headers from the body — a keyword, a byte count, or a blank line?
The response mirrors the request. Its first line is the status line: version, a three-digit status code, and a human reason phrase.
HTTP/1.1 200 OK <- status line: VERSION CODE REASON
Content-Type: text/html <- header
Content-Length: 1256 <- header (body length in bytes)
<- BLANK LINE
<html>...</html> <- body
The Content-Length header tells the client how many body bytes to read. The reason phrase (OK, Not Found) is for humans only — programs act on the numeric code.
Knowledge check (predict the output): A health-checker reads only the first line of a response and splits it on spaces. Given
HTTP/1.1 404 Not Found, what are the three pieces, and which one is the status code?
The first digit tells you the category. Memorize the classes; you can look up individual codes.
| Class | Meaning | Examples | What a client typically does |
|---|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content | Use the body |
| 3xx | Redirect | 301 Moved, 302 Found, 304 Not Modified | Follow the Location header / use cache |
| 4xx | Client error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found | Fix the request; do not blindly retry |
| 5xx | Server error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable | Back off and retry later |
Pitfall: treating 401 and 403 as the same. 401 Unauthorized means "you are not authenticated — log in." 403 Forbidden means "you are authenticated, but you are not allowed." The distinction changes what the client should do next.
Before any HTTP bytes flow over an HTTPS connection, TLS runs a handshake to build a secure tunnel. TLS provides three guarantees:
| Guarantee | What it means | How TLS provides it |
|---|---|---|
| Confidentiality | Eavesdroppers cannot read the traffic | Symmetric encryption with a per-session key |
| Integrity | Tampering is detected | Message authentication codes / AEAD |
| Authentication | The server is who it claims to be | A certificate signed by a trusted Certificate Authority (CA) |
TLS does not guarantee availability — it cannot stop a server from crashing or being flooded offline. (That is exactly the point of this lesson's quiz.)
Here is the handshake at a high level:
Client Server
| --- ClientHello (versions, ciphers, SNI) ---> |
| <-- ServerHello (chosen cipher) ------------ |
| <-- Certificate (server's public cert chain) |
| --- key exchange messages -----------------> <-- |
| === both sides derive the same session key === |
| <===== encrypted HTTP now flows =============> |
The SNI (Server Name Indication) field in the ClientHello is sent in plaintext so the server knows which site's certificate to present — one IP can serve many domains. The server proves its identity by sending a certificate whose chain leads up to a CA your system already trusts. Your client checks: is the signature valid, is it within the validity dates, and does the hostname match a name in the certificate?
When NOT to disable verification: in real code, never turn off certificate checking to "make it work." That silently removes the authentication guarantee and invites a man-in-the-middle. Fix the trust store instead.
Knowledge check (explain in your own words): TLS gives confidentiality, integrity, and authentication. Why does none of those stop an attacker from taking a server offline?
A certificate is public — anyone who connects sees it. Useful fields:
example.com, www.example.com, api.example.com.For an operator, these fields catch real bugs: a certificate that expired last night, or one whose SAN list does not include the hostname users actually type (which makes browsers show a scary warning).
Pitfall: assuming a valid certificate means the site is safe. A certificate only proves you are talking to the owner of that name over an encrypted channel. A phishing site can hold a perfectly valid certificate for its own look-alike domain.
HTTP has no C-style syntax — it is a text format. The structure to internalize:
<start-line>\r\n request line OR status line
<Header-Name>: <value>\r\n zero or more headers
<Header-Name>: <value>\r\n
\r\n blank line: END OF HEADERS
<optional body bytes> length given by Content-Length
Key rules:
\r\n (CRLF), not just \n.\r\n\r\n.Host: and host: are the same.METHOD SP PATH SP VERSION; a status line is VERSION SP CODE SP REASON (SP = a single space).URLs also have structure: https://example.com:443/path?query#fragment — scheme, host, optional port, path, optional query, optional fragment.
HTTP is the request/response protocol of the web (port 80). HTTPS is the same protocol wrapped in TLS for encryption (port 443). TLS (Transport Layer Security) is the standard that encrypts traffic between client and server.
A request starts with a request line, followed by headers:
GET /index.html HTTP/1.1
Host: example.com
User-Agent: curl/8.0
Accept: */*
A blank line ends the headers. A body, if there is one, follows after it.
The response has the same shape:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1256
<html>...</html>
Status codes are grouped by their first digit:
TLS provides three guarantees:
Before any HTTP flows, a TLS handshake runs. It negotiates a cipher, validates the certificate chain, and derives the session keys.
The certificate also leaks useful recon: hostnames in the SAN field, the issuer, and the validity dates.
Web application testing is largely about reading and manipulating these requests and responses.
Headers, status codes, and cookies are where most web vulnerabilities surface. TLS misconfigurations are findings in their own right — for example, weak ciphers, or certificates that are expired or do not match the hostname.
#include <stdio.h> #include <string.h> #include <ctype.h>
/*
Parse the status LINE of an HTTP response: "HTTP/1.1 404 Not Found".
Writes the numeric code to *out_code and the reason phrase into
out_reason (a buffer of size cap). Returns 0 on success, -1 on a
malformed line. This is the first thing a health-checker reads. */ int http_parse_status(const char *line, int *out_code, char out_reason, size_t cap) { if (line == NULL || out_code == NULL || out_reason == NULL || cap == 0) return -1; / defensive: no NULLs */
/* 1) The version token must start with "HTTP/" */ if (strncmp(line, "HTTP/", 5) != 0) return -1;
/* 2) Skip the version token up to the first space */ const char p = strchr(line, ' '); if (p == NULL) return -1; p++; / now at the code */
/* 3) Read exactly a 3-digit code */ int code = 0, digits = 0; while (isdigit((unsigned char)p)) { / cast: isdigit wants unsigned */ code = code * 10 + (p - '0'); p++; digits++; } if (digits != 3) return -1; / HTTP codes are 3 digits */ *out_code = code;
/* 4) One space, then the reason phrase runs to end of line (\r or \n) */ if (*p != ' ') return -1; p++;
size_t i = 0; while (*p != '\0' && *p != '\r' && *p != '\n' && i + 1 < cap) { out_reason[i++] = p++; / i+1<cap leaves room for '\0' / } out_reason[i] = '\0'; / always NUL-terminate */ return 0; }
int main(void) { const char responses[] = { "HTTP/1.1 200 OK\r\n", "HTTP/1.1 404 Not Found\r\n", "HTTP/1.1 503 Service Unavailable\r\n", "GARBAGE not a status line\r\n" / should fail */ }; size_t n = sizeof(responses) / sizeof(responses[0]);
for (size_t k = 0; k < n; k++) {
int code = 0;
char reason[64];
if (http_parse_status(responses[k], &code, reason, sizeof reason) == 0) {
const char *class_name =
code / 100 == 2 ? "success" :
code / 100 == 3 ? "redirect" :
code / 100 == 4 ? "client error" :
code / 100 == 5 ? "server error" : "unknown";
printf("code=%d reason=\"%s\" class=%s\n", code, reason, class_name);
} else {
printf("malformed status line: %.20s...\n", responses[k]);
}
}
return 0;
}
/*
The program parses four status lines and classifies each code. Walk through http_parse_status:
NULL pointers and a zero-size buffer immediately. This is the cheapest bug prevention there is.strncmp(line, "HTTP/", 5) compares the first five bytes. If they are not HTTP/, this is not a status line, so we return -1. That is why the "GARBAGE..." input fails.strchr(line, ' ') returns a pointer to the space after the version token (HTTP/1.1). p++ steps past the space so p now points at the first digit of the code.while (isdigit(...)) loop builds the number: code = code*10 + (*p - '0'). For 404 it computes 0 -> 4 -> 40 -> 404. We count digits and insist on exactly 3.out_reason until we hit end-of-line or run out of buffer. The condition i + 1 < cap guarantees room for the terminating '\0'.Here is the pointer walking across HTTP/1.1 404 Not Found:
| Step | *p points at |
code |
Notes |
|---|---|---|---|
| after strchr+1 | 4 |
0 | start of code |
| loop 1 | 0 |
4 | 0*10+4 |
| loop 2 | 4 |
40 | 4*10+0 |
| loop 3 | (space) |
404 | 40*10+4, loop stops (not a digit) |
| reason copy | N |
404 | copies Not Found |
Back in main, code / 100 maps the code to a class: 404 / 100 == 4, so it prints class=client error. Integer division is the whole trick behind the status-class table.
Mistake 1 — splitting the response on \n alone.
/* WRONG: assumes lines end in \n and forgets the \r */
char *nl = strchr(response, '\n');
*nl = '\0'; /* leaves a stray '\r' at the end of the line */
HTTP lines end in \r\n. If you split on \n only, a trailing \r clings to your token and later comparisons like strcmp(reason, "OK") fail mysteriously. Fix: stop at either \r or \n (as the lesson code does), or trim the \r explicitly. Recognize it: values that "look right" in a debugger but never compare equal.
Mistake 2 — trusting Content-Length blindly.
/* WRONG: read exactly Content-Length bytes with no sanity check */
char *body = malloc(content_length); /* what if it's huge or negative? */
fread(body, 1, content_length, sock);
A hostile or buggy server can send a Content-Length that is enormous or does not match the real body. Allocating or reading on that number unchecked leads to memory exhaustion or a hang. Fix: validate the header is a non-negative integer and cap it against a sane maximum before allocating.
Mistake 3 — disabling certificate verification to "fix" TLS.
curl --insecure https://api.example.com # or SSL_VERIFY_NONE in code
This makes the error go away and silently removes the authentication guarantee — now any machine on the path can impersonate the server. Fix: install or point to the correct CA bundle; only ever disable verification against a throwaway test server you fully control. Recognize it: "it works after I turned off SSL checking" is a red flag, not a solution.
Mistake 4 — confusing 401 and 403 in a client retry loop. Retrying a 403 with the same credentials will never succeed (you lack permission), while retrying a 401 after logging in might. Treating them the same causes infinite retry storms.
Compiler errors (for the C example):
implicit declaration of function 'isdigit' → you forgot #include <ctype.h>.char to isdigit can trigger -Wchar-subscripts or undefined behavior on negative values; cast to unsigned char as the code does.Runtime errors:
i + 1 < cap, not i < cap. Run under AddressSanitizer (gcc -fsanitize=address) and it will pinpoint the write.Logic errors:
p points at a digit right before the digit loop.\n\n but the real delimiter is \r\n\r\n.Debugging a live HTTPS problem — questions to ask:
notAfter.openssl s_client -connect host:443 print the negotiated version, the certificate chain, and the SANs — the fastest way to see what the server actually presents.Parsing untrusted network text in C is a classic source of memory-safety bugs. For this topic specifically:
i + 1 < cap so it can never write past out_reason, and it always writes a terminating '\0'. Any time you copy header or body bytes into a fixed buffer, the copy length must be bounded by the destination size, not by the input.Content-Length, chunk sizes, and TLS record lengths all come from the other side. Validate them (non-negative, within a maximum, consistent with bytes actually received) before using them to allocate or index. An unchecked length is how buffer overflows and out-of-memory crashes happen.strchr/strcmp on a buffer that is not terminated reads out of bounds. Terminate deliberately, or use length-bounded functions.out_code and out_reason are written only on the success path; the caller must check the return value before reading them. Returning -1 without touching the outputs is fine as long as the contract ("check the return first") is documented and followed.isdigit/isspace want unsigned char. Passing a possibly-negative char is undefined behavior. Cast, as the example does.Defensive/security practices for HTTP and TLS:
Where this shows up in real systems:
Host header and path.notAfter, and alert on certificates about to expire — a very common outage cause.Professional habits:
\r\n, always send the Host header, check the return value of your parser before using its outputs, and read status codes by class rather than memorizing every number.status_code, reason_phrase), and clean up sockets and buffers on every path, including errors.Beginner 1 — Classify a code.
Write a function const char *status_class(int code) that returns "success", "redirect", "client error", "server error", or "unknown". Requirements: use integer division by 100; handle codes outside 100–599 as "unknown". Example: status_class(204) -> "success", status_class(302) -> "redirect". Concepts: status classes, integer division.
Beginner 2 — Find the blank line.
Given a full HTTP response in a char *, write a function that returns a pointer to the first body byte (the byte right after \r\n\r\n), or NULL if the header terminator is not present. Requirement: search for the exact 4-byte sequence. Hint: strstr(response, "\r\n\r\n"), then add 4. Concepts: header/body boundary, CRLF.
Intermediate 1 — Build a GET request.
Implement int build_http_get(const char *host, const char *path, char *out, size_t cap) that writes a minimal, valid HTTP/1.1 GET request into out. It must include the request line, a Host header, and end with a blank line. Requirements: never write past cap; return the number of bytes written or -1 on truncation. Example output for host example.com, path /: GET / HTTP/1.1\r\nHost: example.com\r\n\r\n. Hint: snprintf returns the length it would have written — compare it to cap. Concepts: request structure, bounded formatting.
Intermediate 2 — Extract a header value.
Write int get_header(const char *response, const char *name, char *out, size_t cap) that finds a header (case-insensitive) and copies its value (trimmed) into out. Requirements: search only within the header section (before \r\n\r\n); return 0 on success, -1 if the header is absent. Input/output: for header Content-Type, extract text/html. Hint: match name followed by :, then skip spaces. Concepts: headers, case-insensitive matching, bounds.
Challenge — Certificate summary reader (concept + safety).
Without doing any live networking, write a small program that takes a pre-parsed structure describing a certificate (its SAN list, issuer string, and notAfter date as YYYY-MM-DD) and a hostname + today's date, and prints a verdict: VALID, EXPIRED, or HOSTNAME MISMATCH. Requirements: hostname must exactly match one SAN entry; the date comparison must treat an equal date as still valid; handle an empty SAN list as a mismatch. Hint: compare dates as strings since YYYY-MM-DD sorts lexicographically. Concepts: certificate fields (SAN, validity), authentication logic, defensive checks. Do NOT connect to any real server — operate only on the supplied data.
\r\n, and \r\n\r\n ends the headers.code / 100 gives you the class.\r\n, always send Host, never trust length fields from the network, bound every copy and NUL-terminate, and keep certificate verification ON.