לדלג לתוכן

פרוטוקול HTTP - HTTP protocol

הקדמה

HTTP (HyperText Transfer Protocol) הוא הפרוטוקול שעליו בנוי כל האינטרנט. כשאתם גולשים לאתר, הדפדפן שלכם שולח בקשת HTTP לשרת ומקבל תשובת HTTP חזרה. זה הכל.

והנה הדבר המפתיע: HTTP הוא פשוט טקסט על גבי TCP. אין פורמט בינארי מסובך, אין הצפנה מובנית (ב-HTTP רגיל), אין קסם. זה פשוט מחרוזות טקסט שנשלחות דרך סוקט TCP.

בהרצאה הזו נבנה גם לקוח HTTP וגם שרת HTTP מאפס, באמצעות כל מה שלמדנו בפרקים הקודמים.


מבנה בקשת HTTP - HTTP request

בקשת HTTP היא מחרוזת טקסט עם מבנה קבוע:

METHOD PATH HTTP/VERSION\r\n
Header1: value1\r\n
Header2: value2\r\n
\r\n
[body]

שורת הבקשה - request line

השורה הראשונה מכילה שלושה חלקים:

  1. מתודה - method - מה רוצים לעשות:
  2. GET - קבלת משאב (הנפוץ ביותר - טעינת דף)
  3. POST - שליחת נתונים (טפסים, העלאת קבצים)
  4. PUT - עדכון משאב
  5. DELETE - מחיקת משאב
  6. HEAD - כמו GET אבל בלי ה-body (רק headers)

  7. נתיב - path - איזה משאב רוצים: /index.html, /api/users, /images/logo.png

  8. גרסה - version - HTTP/1.0 או HTTP/1.1 (בדרך כלל 1.1)

דוגמה:

GET /index.html HTTP/1.1\r\n

כותרות - headers

אחרי שורת הבקשה באות כותרות (headers) - מידע נוסף על הבקשה. כל כותרת בשורה נפרדת, בפורמט Key: Value:

Host: www.example.com\r\n
User-Agent: MyClient/1.0\r\n
Accept: text/html\r\n
Connection: close\r\n

הכותרת 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

התשובה דומה במבנה:

HTTP/VERSION STATUS_CODE REASON\r\n
Header1: value1\r\n
Header2: value2\r\n
\r\n
[body]

שורת המצב - status line

HTTP/1.1 200 OK\r\n
  • גרסה - 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\r\n
Content-Length: 1234\r\n
Server: MyServer/1.0\r\n
Connection: close\r\n

כותרות חשובות:
- 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 שמוריד דף אינטרנט. הרעיון פשוט:

  1. ליצור חיבור TCP לשרת (פורט 80)
  2. לשלוח בקשת HTTP כטקסט
  3. לקרוא את התשובה
  4. לפרסר אותה (להפריד 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;
}

קומפילציה והרצה:

gcc -o httpclient httpclient.c
./httpclient example.com /

שימו לב ל-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

נקודות חשובות בשרת

  1. הגנה מpath traversal - בודקים שהנתיב לא מכיל ... בלי זה, לקוח יכול לבקש GET /../../etc/passwd ולקרוא קבצים מחוץ לתיקיית השרת. זו פגיעות אבטחה קלאסית.

  2. Content-Type - חשוב לשלוח את סוג התוכן הנכון. הדפדפן משתמש בזה כדי להחליט איך להציג את התוכן.

  3. 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 - אותו פרוטוקול, עם שכבת הצפנה