פרוטוקול HTTP - HTTP protocol¶
הקדמה¶
HTTP (HyperText Transfer Protocol) הוא הפרוטוקול שעליו בנוי כל האינטרנט. כשאתם גולשים לאתר, הדפדפן שלכם שולח בקשת HTTP לשרת ומקבל תשובת HTTP חזרה. זה הכל.
והנה הדבר המפתיע: HTTP הוא פשוט טקסט על גבי TCP. אין פורמט בינארי מסובך, אין הצפנה מובנית (ב-HTTP רגיל), אין קסם. זה פשוט מחרוזות טקסט שנשלחות דרך סוקט TCP.
בהרצאה הזו נבנה גם לקוח HTTP וגם שרת HTTP מאפס, באמצעות כל מה שלמדנו בפרקים הקודמים.
מבנה בקשת HTTP - HTTP request¶
בקשת HTTP היא מחרוזת טקסט עם מבנה קבוע:
שורת הבקשה - request line¶
השורה הראשונה מכילה שלושה חלקים:
- מתודה - method - מה רוצים לעשות:
GET- קבלת משאב (הנפוץ ביותר - טעינת דף)POST- שליחת נתונים (טפסים, העלאת קבצים)PUT- עדכון משאבDELETE- מחיקת משאב-
HEAD- כמו GET אבל בלי ה-body (רק headers) -
נתיב - path - איזה משאב רוצים:
/index.html,/api/users,/images/logo.png -
גרסה - version -
HTTP/1.0אוHTTP/1.1(בדרך כלל 1.1)
דוגמה:
כותרות - headers¶
אחרי שורת הבקשה באות כותרות (headers) - מידע נוסף על הבקשה. כל כותרת בשורה נפרדת, בפורמט Key: Value:
הכותרת Host חובה ב-HTTP/1.1. היא אומרת לשרת לאיזה דומיין הבקשה מיועדת (שרת אחד יכול לארח כמה אתרים).
שורה ריקה¶
שורה ריקה (\r\n) מפרידה בין הכותרות לגוף הבקשה. זה הסימן "הכותרות נגמרו".
גוף - body¶
אופציונלי. בקשות GET בדרך כלל בלי body. בקשות POST כוללות body עם הנתונים שנשלחים.
דוגמה מלאה¶
GET /index.html HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: MyClient/1.0\r\n
Accept: */*\r\n
Connection: close\r\n
\r\n
מבנה תשובת HTTP - HTTP response¶
התשובה דומה במבנה:
שורת המצב - status line¶
- גרסה -
HTTP/1.1 - קוד מצב - status code - מספר שמתאר את התוצאה
- סיבה - reason phrase - תיאור טקסטואלי (לנוחות, לא חובה)
קודי מצב נפוצים¶
| קוד | שם | משמעות |
|---|---|---|
| 200 | OK | הבקשה הצליחה |
| 301 | Moved Permanently | המשאב עבר כתובת (redirect קבוע) |
| 302 | Found | המשאב עבר כתובת (redirect זמני) |
| 400 | Bad Request | בקשה שגויה |
| 403 | Forbidden | אין הרשאה |
| 404 | Not Found | המשאב לא נמצא |
| 500 | Internal Server Error | שגיאה בשרת |
כותרות תשובה¶
כותרות חשובות:
- Content-Type - סוג התוכן: text/html, text/plain, image/png, application/json
- Content-Length - אורך הbody בבתים. הלקוח יודע כמה בתים לקרוא
דוגמה מלאה¶
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 44\r\n
\r\n
<html><body><h1>Hello!</h1></body></html>
בניית לקוח HTTP בC¶
עכשיו שאנחנו מבינים את הפרוטוקול, בואו נבנה לקוח HTTP שמוריד דף אינטרנט. הרעיון פשוט:
- ליצור חיבור TCP לשרת (פורט 80)
- לשלוח בקשת HTTP כטקסט
- לקרוא את התשובה
- לפרסר אותה (להפריד headers מ-body)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define BUFFER_SIZE 4096
/* resolve hostname to IP address */
int resolve_hostname(const char *hostname, struct sockaddr_in *addr)
{
struct hostent *he = gethostbyname(hostname);
if (!he) {
fprintf(stderr, "cannot resolve: %s\n", hostname);
return -1;
}
addr->sin_family = AF_INET;
memcpy(&addr->sin_addr, he->h_addr_list[0], he->h_length);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "usage: %s <hostname> <path>\n", argv[0]);
fprintf(stderr, "example: %s example.com /index.html\n", argv[0]);
exit(1);
}
const char *hostname = argv[1];
const char *path = argv[2];
/* resolve hostname */
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
if (resolve_hostname(hostname, &server_addr) == -1)
exit(1);
server_addr.sin_port = htons(80);
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &server_addr.sin_addr, ip, sizeof(ip));
printf("connecting to %s (%s)...\n", hostname, ip);
/* TCP connection */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(1);
}
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
close(sockfd);
exit(1);
}
/* build the HTTP request */
char request[1024];
snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: MyHTTPClient/1.0\r\n"
"Accept: */*\r\n"
"Connection: close\r\n"
"\r\n",
path, hostname);
printf("sending request:\n%s", request);
/* send the request */
write(sockfd, request, strlen(request));
/* read the response */
char response[BUFFER_SIZE * 16]; /* large buffer for the response */
int total = 0;
ssize_t n;
while ((n = read(sockfd, response + total, sizeof(response) - total - 1)) > 0) {
total += n;
}
response[total] = '\0';
printf("\nreceived %d bytes\n", total);
/* find the separator between headers and body */
char *body = strstr(response, "\r\n\r\n");
if (body) {
*body = '\0'; /* terminate the headers */
body += 4; /* skip past \r\n\r\n */
printf("\n=== HEADERS ===\n%s\n", response);
printf("\n=== BODY (%ld bytes) ===\n%s\n", strlen(body), body);
} else {
printf("could not parse response\n");
}
close(sockfd);
return 0;
}
קומפילציה והרצה:
שימו לב ל-Connection: close. זה אומר לשרת "אחרי שתשלח את התשובה, סגור את החיבור". בלי זה, השרת עלול להשאיר את החיבור פתוח (keep-alive) ו-read לא יחזיר 0.
בניית שרת HTTP בC¶
עכשיו נבנה שרת HTTP שמגיש קבצים סטטיים. שרת שמקבל בקשת GET, מוצא את הקובץ המבוקש, ומחזיר אותו עם headers מתאימים.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 4096
#define ROOT_DIR "./www" /* root directory for files */
/* detect content type from extension */
const char *get_content_type(const char *path)
{
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0)
return "text/html";
if (strcmp(ext, ".css") == 0)
return "text/css";
if (strcmp(ext, ".js") == 0)
return "application/javascript";
if (strcmp(ext, ".json") == 0)
return "application/json";
if (strcmp(ext, ".png") == 0)
return "image/png";
if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(ext, ".gif") == 0)
return "image/gif";
if (strcmp(ext, ".txt") == 0)
return "text/plain";
return "application/octet-stream";
}
/* send error response */
void send_error(int client_fd, int code, const char *reason)
{
char body[256];
snprintf(body, sizeof(body),
"<html><body><h1>%d %s</h1></body></html>", code, reason);
char response[512];
snprintf(response, sizeof(response),
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
code, reason, strlen(body), body);
write(client_fd, response, strlen(response));
}
/* handle the request */
void handle_request(int client_fd)
{
char buffer[BUFFER_SIZE];
ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n <= 0) return;
buffer[n] = '\0';
/* parse the request line */
char method[16], path[256], version[16];
if (sscanf(buffer, "%15s %255s %15s", method, path, version) != 3) {
send_error(client_fd, 400, "Bad Request");
return;
}
printf("%s %s %s\n", method, path, version);
/* only support GET */
if (strcmp(method, "GET") != 0) {
send_error(client_fd, 405, "Method Not Allowed");
return;
}
/* protection against path traversal: .. is not allowed in the path */
if (strstr(path, "..")) {
send_error(client_fd, 403, "Forbidden");
return;
}
/* build the full path */
char filepath[512];
if (strcmp(path, "/") == 0)
snprintf(filepath, sizeof(filepath), "%s/index.html", ROOT_DIR);
else
snprintf(filepath, sizeof(filepath), "%s%s", ROOT_DIR, path);
/* open the file */
FILE *fp = fopen(filepath, "rb");
if (!fp) {
send_error(client_fd, 404, "Not Found");
return;
}
/* find the file size */
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
/* send headers */
char headers[512];
snprintf(headers, sizeof(headers),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
get_content_type(filepath), file_size);
write(client_fd, headers, strlen(headers));
/* send the file content */
char file_buf[BUFFER_SIZE];
size_t bytes;
while ((bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0) {
write(client_fd, file_buf, bytes);
}
fclose(fp);
}
int main(void)
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(1);
}
if (listen(server_fd, 10) == -1) {
perror("listen");
exit(1);
}
printf("HTTP server on port %d, serving files from %s\n", PORT, ROOT_DIR);
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept");
continue;
}
handle_request(client_fd);
close(client_fd);
}
close(server_fd);
return 0;
}
לבדיקה:
# create a directory and a page
mkdir -p www
echo '<html><body><h1>Hello from C!</h1></body></html>' > www/index.html
# compile and run
gcc -o httpserver httpserver.c
./httpserver
# in another terminal, or in a browser:
curl http://localhost:8080/
curl http://localhost:8080/index.html
נקודות חשובות בשרת¶
-
הגנה מpath traversal - בודקים שהנתיב לא מכיל
... בלי זה, לקוח יכול לבקשGET /../../etc/passwdולקרוא קבצים מחוץ לתיקיית השרת. זו פגיעות אבטחה קלאסית. -
Content-Type - חשוב לשלוח את סוג התוכן הנכון. הדפדפן משתמש בזה כדי להחליט איך להציג את התוכן.
-
Content-Length - אומר ללקוח כמה בתים לצפות. בלי זה, הלקוח לא יודע מתי התוכן נגמר (חוץ מלחכות לסגירת החיבור).
שרת HTTP עם epoll¶
השרת שלנו מטפל בלקוח אחד בכל פעם. בואו נשלב אותו עם epoll מפרק 11.2 כדי לטפל בכמה לקוחות במקביל:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define PORT 8080
#define MAX_EVENTS 64
#define BUFFER_SIZE 4096
#define ROOT_DIR "./www"
void set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
const char *get_content_type(const char *path)
{
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
if (strcmp(ext, ".html") == 0) return "text/html";
if (strcmp(ext, ".css") == 0) return "text/css";
if (strcmp(ext, ".js") == 0) return "application/javascript";
if (strcmp(ext, ".png") == 0) return "image/png";
if (strcmp(ext, ".jpg") == 0) return "image/jpeg";
if (strcmp(ext, ".txt") == 0) return "text/plain";
return "application/octet-stream";
}
void send_response(int fd, int code, const char *reason,
const char *content_type, const char *body, long body_len)
{
char headers[512];
int hlen = snprintf(headers, sizeof(headers),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
code, reason, content_type, body_len);
write(fd, headers, hlen);
if (body && body_len > 0)
write(fd, body, body_len);
}
void handle_request(int client_fd)
{
char buffer[BUFFER_SIZE];
ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n <= 0) return;
buffer[n] = '\0';
char method[16], path[256];
if (sscanf(buffer, "%15s %255s", method, path) != 2) {
const char *err = "<h1>400 Bad Request</h1>";
send_response(client_fd, 400, "Bad Request", "text/html", err, strlen(err));
return;
}
if (strcmp(method, "GET") != 0) {
const char *err = "<h1>405 Method Not Allowed</h1>";
send_response(client_fd, 405, "Method Not Allowed", "text/html", err, strlen(err));
return;
}
if (strstr(path, "..")) {
const char *err = "<h1>403 Forbidden</h1>";
send_response(client_fd, 403, "Forbidden", "text/html", err, strlen(err));
return;
}
char filepath[512];
if (strcmp(path, "/") == 0)
snprintf(filepath, sizeof(filepath), "%s/index.html", ROOT_DIR);
else
snprintf(filepath, sizeof(filepath), "%s%s", ROOT_DIR, path);
FILE *fp = fopen(filepath, "rb");
if (!fp) {
const char *err = "<h1>404 Not Found</h1>";
send_response(client_fd, 404, "Not Found", "text/html", err, strlen(err));
return;
}
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
char headers[512];
int hlen = snprintf(headers, sizeof(headers),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
get_content_type(filepath), file_size);
write(client_fd, headers, hlen);
char file_buf[BUFFER_SIZE];
size_t bytes;
while ((bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0)
write(client_fd, file_buf, bytes);
fclose(fp);
}
int main(void)
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 64);
set_nonblocking(server_fd);
int epoll_fd = epoll_create1(0);
struct epoll_event ev = {.events = EPOLLIN, .data.fd = server_fd};
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
printf("HTTP server (epoll) on port %d\n", PORT);
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_fd) {
/* new connection */
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd;
while ((client_fd = accept(server_fd,
(struct sockaddr *)&client_addr, &len)) != -1) {
ev.events = EPOLLIN;
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
}
} else {
/* request from a client */
handle_request(events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
}
}
}
return 0;
}
עכשיו השרת מטפל בכמה חיבורים במקביל, ב-thread אחד. זה הדפוס הבסיסי שעליו בנויים שרתים כמו nginx.
הערה על HTTPS¶
HTTPS הוא HTTP + TLS (Transport Layer Security). TLS מצפין את החיבור כך שאף אחד באמצע לא יכול לקרוא את התעבורה.
מבחינת הקוד שלנו, ההבדל הוא בשכבת התחבורה: במקום read/write ישירות על הסוקט, משתמשים בפונקציות של ספריית הצפנה (כמו OpenSSL) שמצפינות ומפענחות את הנתונים. חלק הHTTP עצמו - הבקשות, התשובות, הheaders - נשאר בדיוק אותו דבר.
ספריות כמו OpenSSL מספקות API שנראה דומה מאוד ל-read/write:
/* instead of: */
write(sockfd, data, len);
read(sockfd, buffer, size);
/* use: */
SSL_write(ssl, data, len);
SSL_read(ssl, buffer, size);
לא ניכנס למימוש TLS בקורס הזה, אבל חשוב להבין את העיקרון: HTTPS הוא אותו HTTP, רק עטוף בשכבת הצפנה.
סיכום¶
- HTTP הוא פרוטוקול טקסט פשוט על גבי TCP
- בקשה: שורת בקשה (method + path + version), headers, שורה ריקה, body
- תשובה: שורת מצב (version + status code + reason), headers, שורה ריקה, body
\r\nמפריד בין שורות,\r\n\r\nמפריד בין headers ל-body- לקוח HTTP: חיבור TCP לפורט 80, שליחת בקשה כטקסט, קריאת תשובה
- שרת HTTP: האזנה, קבלת חיבור, קריאת בקשה, שליחת קובץ עם headers מתאימים
- בדיקת path traversal (
..) היא הגנת אבטחה בסיסית אבל קריטית - שילוב עם epoll הופך את השרת ליעיל לטיפול בהרבה חיבורים
- HTTPS = HTTP + TLS - אותו פרוטוקול, עם שכבת הצפנה