Network Applications: HTTP
Application Protocols
Application Layer
In the network protocol stack, the application layer is at the top.
- It consists of applications: web browsers/servers, email clients/servers, P2P file sharing apps, etc.
Application protocols: define how peer applications communicate with each other.
- Example: HTTP.
HTTP
Invented by Tim Berners-Lee at CERN in 1989 with the goal of sharing scientific data online.
The important content type is HTML: HyperText Markup Langauge, but is flexible enough for access to any kind of data.
Mentioned before,
HTTP is the application protocol underlying the Word Wide Web. HTTP is a synchronous client/server protocol used by web browsers, web servers, web clients, and web services.
Clients send request to server, and server sends back a response.
- The request specifies a verb (GET, POST, PUT, etc.) and a name of a resource.
Requests and responses may have a body containing data.
- The content type specifies what kind of data the body contains.
The best way to learn about HTTP is to examine actual HTTP message exchanges. The curl program is a command-line HTTP client: use the -v option to have it print the first line and headers of the HTTP request and HTTP response.
- E.g.,
curl -v http://placekitten.com/1920/1080 -o kitten.jpg
Message Format
All HTTP Messages have the same general form:
- First Line: describes meaning of message
- Zero or more headers: metadata about the message
- Optional body: payload of actual application data (HTML document, image, etc.)
Protocol is text-based, with lines used to delimit important structures.
- each line terminated by CR (ASCII 13) followed by LF (ASCII 10).
- Line continuation using backslash (
\) allowed for headers.
An HTTP header has the form
Name: Content
Each header provides metadata to help recipient understand the meaning of the message.
- E.g.,
Host: placekitten.comspecifies which host server is accessed.Content-Type: text/htmlspecify that body is an HTML document.
Request
An HTTP request is a message from a client to a server.
Specifies a method and a resource.
- method: verb specifying what action the client is requesting the server perform (GET, POST, PUT, etc.)
- resource: the data resource on the server to which the client is requesting access.
For HTTP 1.1, the first line also specifies the protocol version.
A request can have a body (payload): E.g., submitted form data, file upload data.
Example:
GET /1024/768 HTTP/1.1
Host: placekitten.com
User-Agent: curl/7.58.0
Accept: */*
GETis the method/1024/768is the resourceHostheader specifies which website is being accessed (a web server can host multiple sites).- The
Acceptheader indicates what file types the client is prepared to receive.
Response
An HTTP Response indicates protocol version, status code, and reason phrase.
- status code specifies how the client shoud interpret the response. E.g., 200 (OK), 403 (Forbidden), 404 (Not Found), and more
- reason phrase is informational and does not affect the meaning of the response.
Example response
HTTP/1.1 200 OK
Date: Wed, 13 Nov 2019 12:33:20 GMT
Content-Type: image/jpeg
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: __cfduid=de2a22cdd3ed939398e0a56f41ce0e4a31573648400; expires=T
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
Expires: Thu, 31 Dec 2020 20:00:00 GMT
CF-Cache-Status: HIT
Age: 51062
Server: cloudflare
CF-RAY: 5350c608682a957e-IAD
This is followed by a blank line and 40,473 bytes of data.
- Response code is
200, indicating success. Content-Typeindicates resource is an imageTransfer-Encodingheader indicates that the body is encoding using "chunked" encoding (common for streaming content, but also used for static content)Connection: keep-aliveheader invites the client to keep the connection open, to be reused for subsequent requests. TCP connections are pretty expensive, so it makes sense to reuse.
The Content-Type header indicates what kind of data the message body contains. It is a MIME type.
- E.g.,
text/htmlHTML document,text/html; charset=utf-8HTML document with UTF-8 character set,image/jpegJPEG image. - Official registry of MIME types.
Message Body
An HTTP request/response can have a body contianing arbitrary data. Compression can be used. Various encodings are possible:
- raw binary
- chunked (chunks consist of byte count followed by specified amount of data)
HTTP Server Implementation
HTTP Server: listens for incoming TCP connections, reads client requests, sends back responses.
The server loop:
int main(int argc, char **argv) {
if (argc != 3) { fatal("Usage: webserver <port> <webroot>"); }
const char *port = argv[1];
const char *webroot = argv[2];
int serverfd = open_listenfd((char*) port);
if (serverfd < 0) { fatal("Couldn't open server socket"); }
while (1) {
int clientfd = Accept(serverfd, NULL, NULL);
if (clientfd < 0) { fatal("Error accepting client connection"); }
server_chat_with_client(clientfd, webroot);
close(clientfd);
}
}
open_listenfd: create server socketAccept: wait for incoming connection
Chat with client: The server_chat_with_client function reads a client request and generates an appropriate response:
void server_chat_with_client(int clientfd, const char *webroot) {
struct Message *req = NULL;
rio_t in;
rio_readinitb(&in, clientfd);
req = message_read_request(&in);
printf("got request for resource %s\n", req->resource);
if (req) {
server_generate_response(clientfd, req, webroot);
message_destroy(req);
}
}
It’s useful to have data types representing protocol messages:
/* data type for message headers */
struct Header {
char *name;
char *content;
};
/* Message data type, represents a request from a client */
struct Message {
int num_headers; /* number of headers */
struct Header **headers; /* array of headers */
char *method; /* the method */
char *resource; /* the resource requested */
};
- Note that with additional fields,
structMessage could also represent a response
Reading Lines: HTTP uses lines (terminated by CRLF) to structure messages, so a function to read a line of text robustly is very helpful.
ssize_t readline(rio_t *in, char *usrbuf, size_t maxlen) {
ssize_t len = rio_readlineb(in, usrbuf, maxlen);
if (len > 0 && usrbuf[len-1] == '\n') {
/* trim trailing LF (newline) */
usrbuf[len-1] = '\0';
len--;
}
if (len > 0 && usrbuf[len-1] == '\r') {
/* trim trailing CR */
usrbuf[len-1] = '\0';
len--;
}
return len;
}
- Heavy lifting done by
rio_readlineb
Reading a message:
struct Message *message_read_request(rio_t *in) {
struct Message *result = NULL;
// read first line (method and resource)
// read 0 or more headers
// read optional body
return result;
}
- This is a fairly complicated function
Disecting this:
Read first line of request: It has essential infomration including the method, resource, and protocol.
len = readline(in, linebuf, MAX_LINE_LEN);
if (len < 0) { goto read_done; }
char *savep, *method, *resource, *proto;
method = strtok_r(linebuf, " ", &savep);
if (!method) { goto read_done; }
resource = strtok_r(NULL, " ", &savep);
if (!resource) { goto read_done; }
proto = strtok_r(NULL, " ", &savep);
if (!proto || strcmp(proto, "HTTP/1.1") != 0) { goto read_done; }
strtok_ris used to tokenize the line using a set of deliminator.- Error handling simplified using goto: not as awful as it sounds.
Reading a header: body of loop to read 0 or more headers (simplified):
len = readline(in, linebuf, MAX_LINE_LEN);
if (strcmp(linebuf, "") == 0) {
done_with_headers = 1; /* read a blank line */
} else {
/* try to read a header */
char *p = strchr(linebuf, ':');
if (p) {
separate header into name and content parts
struct Header *hdr = create and initialize Header object msg->headers[num_headers] = hdr;
num_headers++;
}
}
- Building a map from header name to the header body
strchrfunction used to find the ':' separating name and content of header- Headers are terminated by a blank line.
Reading the request body: left to your own practice.
Responding to a request:
void server_generate_response(int clientfd, struct Message *req, const char *webroot) {
if (strcmp(req->method, "GET") != 0) {
server_generate_text_response(clientfd, "403", "Forbidden", "only GET requests are allowed");
goto response_done;
}
filename = concat(webroot, req->resource);
struct stat s;
if (stat(filename, &s) < 0) {
server_generate_text_response(clientfd, "404", "Not Found", "requested resource does not exist");
goto response_done;
}
writestr(clientfd, "HTTP/1.1 200 OK\r\n");
// write Content-Type and Content-Length headers
// read data from file and copy to clientfd
response_done:
//cleanup
}
To send a text response: Error messages are sent back as text:
void server_generate_text_response(int clientfd, const char *response_code,
const char *reason, const char *msg) {
writestr(clientfd, "HTTP/1.1 ");
writestr(clientfd, response_code);
writestr(clientfd, " ");
writestr(clientfd, reason);
writestr(clientfd, "\r\n");
/* could generate headers... */
writestr(clientfd, "\r\n");
writestr(clientfd, msg);
}
To put everything together:
$ gcc -o webserver main.c webserver.c csapp.c -lpthread
$ mkdir site
$ curl http://placekitten.com/800/600 -o site/kittens.jpg
Security Vulnerabilityies
Handling Untrusted Data Data read from the client is untrusted.
A network application which connects to untrusted peers must assume that they are malicious.
Therefore, never ever:
- trust that data is properly formatted
- trust no special characters are presented
- trust that message size limits are not exceeded.
Example 1
char buf[1024], field1[256], field2[256];
rio_readlineb(in, buf, 1024);
sscanf(buf, "%s %s", field1, field2);
A buffer overflow occurs when malicious peer sends more data than can be receieved into the recipient's buffer.
If the buffer is stack allocated, then the malicious client could overwrite the return address in the current stack frame with an arbitrary value.
When the function returns, it jumps to an address controlled by the malicious peer.
- i.e., the client can cause the program to call the
systemfunction, which executes an arbitrary program as a subprocess.
HTTP Client Implementation
To develop a simple HTTP Client, it needs to do the following things:
- Parse URL
- Connect to server
- Request resource
- Read headers
- Save body of resource to file.
Pseudocode of main function:
int main(int argc, char **argv) {
// check command line arguments
// parse URL
// connect to server
// send request
// read response headers
// read response body
}
Parse URL
A URL (Uniform Resource Locator) encodes the protocol, name, and location of a data resource.
Example: http://placekitten.com/1024/768
- protocol is
http - host is
placekitten.com - resource name is
/1024/768e
TCP port for (unencrypted) HTTP is 80
- Can be overriden in URL (e.g.,
http://localhost:30000)
The code to parse URL:
/* parse URL */
if (!strncmp(url, "http://", 7) != 0)
{ fatal("only http URLs are supported"); }
url += 7;
char *host, *resource;
host = url;
resource = strchr(host, '/');
if (!resource) { fatal("Invalid URL"); }
*resource = '\0';
resource++;
/* see if a port was specified */
const char *port = "80"; /* default HTTP port */
char *p = strchr(host, ':');
if (p) {
*p = '\0';
port = p+1;
}
Connect, request resource
Once host and port have been determined, connecting and sending request is fairly straightforward:
/* Connect to server */
int connfd = Open_clientfd((char *)host, (char *)port);
/* Send request */
writestr(connfd, "GET /");
writestr(connfd, resource);
writestr(connfd, " HTTP/1.1\r\n");
writestr(connfd, "Host: ");
writestr(connfd, host);
writestr(connfd, "\r\nUser-Agent: jhucsf/0.1\r\n");
writestr(connfd, "Accept: */*\r\n\r\n");
Reading response
The first line = status code:
/* Read response */
char linebuf[LINEBUF_SIZE];
Rio_readlineb(&in, linebuf, LINEBUF_SIZE);
printf("First line: %s\n", linebuf);
/* make sure response code was 200 */
char *p2 = strchr(linebuf, ' ');
if (!p2) { fatal("bad HTTP response?"); }
p2++;
if (strncmp(p2, "200", 3) != 0) { fatal("HTTP response not 200"); }
Client reads headers, looking for Transfer-Encoding and Content-Length
Client needs to know encoding of message body in order to know how to read and decode it
Our client will only support chunked encoding
Headers are terminated by a blank line, message body follows
Reading Message Body
The chunked encoding consists of chunks of binary data, each preceded with a size in bytes (encoded as a hexadecimal integer):
while (1) {
/* determine size of next chunk */
readline(&in, linebuf, LINEBUF_SIZE);
unsigned chunk_len;
sscanf(linebuf, "%x", &chunk_len);
/* if chunk size is 0, body has ended */
if (chunk_len == 0) { break; }
/* read chunk, write to output file */
char *buf = xmalloc(chunk_len);
rio_readnb(&in, buf, chunk_len);
rio_writen(outfd, buf, chunk_len);
free(buf);
/* need to read another \r\n following the chunk */
Rio_readnb(&in, linebuf, 2);
}
Running the program
$ ./webclient http://placekitten.com/1152/864 kitten.jpg
host: placekitten.com
port: 80
resource: /1152/864
First line: HTTP/1.1 200 OK
Header: Date: Sat, 16 Nov 2019 01:15:05 GMT
Header: Content-Type: image/jpeg
Header: Transfer-Encoding: chunked
Chunked encoding
Header: Connection: keep-alive
Header: Set-Cookie: __cfduid=dd8f10ca64c91fce89a52b57aa4ccede71573866904; expires=Sun, 15-Nov-
Header: Access-Control-Allow-Origin: *
Header: Cache-Control: public, max-age=86400
Header: Expires: Thu, 31 Dec 2020 20:00:00 GMT
Header: CF-Cache-Status: MISS
Header: Server: cloudflare
Header: CF-RAY: 53659c9958fbea34-IAD
Download successful?
Linked from Computer System Fundamentals