לדלג לתוכן

11.4 פרוטוקול HTTP פתרון

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

פתרון 1 - לקוח HTTP בסיסי

#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

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s <hostname> <path>\n", argv[0]);
        exit(1);
    }

    const char *hostname = argv[1];
    const char *path = argv[2];

    /* פתרון DNS */
    struct hostent *he = gethostbyname(hostname);
    if (!he) {
        fprintf(stderr, "cannot resolve: %s\n", hostname);
        exit(1);
    }

    /* חיבור */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(80);
    memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("connect");
        close(sockfd);
        exit(1);
    }

    /* שליחת בקשה */
    char request[1024];
    snprintf(request, sizeof(request),
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Connection: close\r\n"
        "User-Agent: MyClient/1.0\r\n"
        "Accept: */*\r\n"
        "\r\n",
        path, hostname);

    write(sockfd, request, strlen(request));

    /* קריאת תשובה */
    char *response = NULL;
    int total = 0;
    int capacity = BUFFER_SIZE;
    response = malloc(capacity);

    ssize_t n;
    while ((n = read(sockfd, response + total, capacity - total - 1)) > 0) {
        total += n;
        if (total >= capacity - 1) {
            capacity *= 2;
            response = realloc(response, capacity);
        }
    }
    response[total] = '\0';
    close(sockfd);

    /* פירסור שורת מצב */
    char version[16], reason[64];
    int status_code;
    sscanf(response, "%15s %d %63[^\r\n]", version, &status_code, reason);
    printf("status: %d %s\n", status_code, reason);

    /* מציאת הפרדה בין headers ל-body */
    char *separator = strstr(response, "\r\n\r\n");
    if (separator) {
        *separator = '\0';
        char *body = separator + 4;

        /* ספירת headers */
        int header_count = 0;
        char *line = response;
        while ((line = strstr(line, "\r\n")) != NULL) {
            header_count++;
            line += 2;
        }

        printf("headers: %d\n", header_count);
        printf("body: %ld bytes\n\n", strlen(body));
        printf("%s\n", body);
    }

    free(response);
    return 0;
}

שימו לב שהשתמשנו ב-malloc/realloc לבאפר דינמי. תשובת HTTP יכולה להיות גדולה מאוד (דף HTML שלם, תמונה), ובאפר קבוע עלול לא להספיק.


פתרון 2 - שרת HTTP סטטי

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 4096

const char *root_dir;

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, ".png") == 0) return "image/png";
    if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, ".txt") == 0) return "text/plain";
    if (strcmp(ext, ".json") == 0) return "application/json";
    return "application/octet-stream";
}

void send_error(int 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];
    int len = 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(fd, response, len);
}

void handle_request(int client_fd, struct sockaddr_in *client_addr)
{
    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) {
        send_error(client_fd, 400, "Bad Request");
        return;
    }

    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));
    printf("%s %s %s\n", client_ip, method, path);

    if (strcmp(method, "GET") != 0) {
        send_error(client_fd, 405, "Method Not Allowed");
        return;
    }

    if (strstr(path, "..")) {
        send_error(client_fd, 403, "Forbidden");
        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) {
        send_error(client_fd, 404, "Not Found");
        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(int argc, char *argv[])
{
    root_dir = (argc > 1) ? argv[1] : "./www";

    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("serving %s on port %d\n", root_dir, PORT);

    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, &client_addr);
        close(client_fd);
    }

    return 0;
}

פתרון 3 - שרת HTTP עם רישום לוג

הפתרון מבוסס על פתרון 2, עם הוספת פונקציית לוג. הנה את החלקים החדשים:

#include <time.h>

FILE *log_file = NULL;

void log_request(const char *client_ip, const char *method,
                 const char *path, int status, long size)
{
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    char timestamp[32];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm);

    /* הדפסה ל-stdout */
    printf("[%s] %s %s %s %d %ld\n", timestamp, client_ip, method, path, status, size);

    /* כתיבה לקובץ */
    if (log_file) {
        fprintf(log_file, "[%s] %s %s %s %d %ld\n",
                timestamp, client_ip, method, path, status, size);
        fflush(log_file);  /* כתיבה מיידית לדיסק */
    }
}

בפונקציית handle_request, אחרי שליחת התשובה:

void handle_request(int client_fd, struct sockaddr_in *client_addr)
{
    /* ... פירסור כמו קודם ... */

    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));

    /* ... טיפול בבקשה ... */

    /* אם שגיאה: */
    if (strstr(path, "..")) {
        send_error(client_fd, 403, "Forbidden");
        log_request(client_ip, method, path, 403, 0);
        return;
    }

    /* ... פתיחת קובץ ... */

    if (!fp) {
        send_error(client_fd, 404, "Not Found");
        log_request(client_ip, method, path, 404, 0);
        return;
    }

    /* אחרי שליחה מוצלחת: */
    log_request(client_ip, method, path, 200, file_size);
}

בmain:

log_file = fopen("access.log", "a");
if (!log_file) {
    perror("cannot open access.log");
    /* ממשיכים בלי לוג */
}

fflush חשוב - בלעדיו, libc עלולה לשמור את הנתונים בבאפר פנימי ולא לכתוב לדיסק מיד. אם השרת קורס, הלוגים האחרונים עלולים ללכת לאיבוד.


פתרון 4 - שרת HTTP עם תמיכה ב-POST

הנה הפונקציה החדשה לטיפול בPOST:

#include <sys/stat.h>

long get_content_length(const char *headers)
{
    const char *cl = strstr(headers, "Content-Length:");
    if (!cl) cl = strstr(headers, "content-length:");
    if (!cl) return -1;
    return strtol(cl + 15, NULL, 10);
}

void handle_post_submit(int client_fd, const char *request, ssize_t request_len)
{
    /* מציאת ההפרדה headers/body */
    const char *body_start = strstr(request, "\r\n\r\n");
    if (!body_start) {
        send_error(client_fd, 400, "Bad Request");
        return;
    }
    body_start += 4;

    long content_length = get_content_length(request);
    long body_in_buffer = request_len - (body_start - request);

    /* פתיחת קובץ הsubmissions */
    FILE *fp = fopen("submissions.txt", "a");
    if (!fp) {
        send_error(client_fd, 500, "Internal Server Error");
        return;
    }

    /* כתיבת מה שכבר יש בבאפר */
    fwrite(body_start, 1, body_in_buffer, fp);

    /* אם יש עוד נתונים לקרוא */
    long remaining = content_length - body_in_buffer;
    char buf[4096];
    while (remaining > 0) {
        ssize_t n = read(client_fd, buf, sizeof(buf));
        if (n <= 0) break;
        fwrite(buf, 1, n, fp);
        remaining -= n;
    }

    fprintf(fp, "\n---\n");  /* מפריד בין submissions */
    fclose(fp);

    /* תשובה */
    const char *body = "<html><body><h1>Submitted!</h1></body></html>";
    char response[512];
    int len = snprintf(response, sizeof(response),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n%s",
        strlen(body), body);
    write(client_fd, response, len);
}

void handle_get_submissions(int client_fd)
{
    FILE *fp = fopen("submissions.txt", "r");
    if (!fp) {
        const char *empty = "<html><body><h1>No submissions yet</h1></body></html>";
        char response[512];
        int len = snprintf(response, sizeof(response),
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: %ld\r\n"
            "Connection: close\r\n"
            "\r\n%s",
            strlen(empty), empty);
        write(client_fd, response, len);
        return;
    }

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    char headers[256];
    int hlen = snprintf(headers, sizeof(headers),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/plain\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n", size);
    write(client_fd, headers, hlen);

    char buf[4096];
    size_t bytes;
    while ((bytes = fread(buf, 1, sizeof(buf), fp)) > 0)
        write(client_fd, buf, bytes);

    fclose(fp);
}

בפונקציה הראשית של טיפול בבקשות, מוסיפים ניתוב:

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];
    sscanf(buffer, "%15s %255s", method, path);

    if (strcmp(method, "POST") == 0 && strcmp(path, "/submit") == 0) {
        handle_post_submit(client_fd, buffer, n);
    } else if (strcmp(method, "GET") == 0 && strcmp(path, "/submissions") == 0) {
        handle_get_submissions(client_fd);
    } else if (strcmp(method, "GET") == 0) {
        /* טיפול רגיל בקבצים סטטיים */
        handle_static_file(client_fd, path);
    } else {
        send_error(client_fd, 405, "Method Not Allowed");
    }
}

פתרון 5 - מוריד קבצים - HTTP downloader

#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
#define MAX_REDIRECTS 5

int connect_to_host(const char *hostname, int port)
{
    struct hostent *he = gethostbyname(hostname);
    if (!he) {
        fprintf(stderr, "cannot resolve: %s\n", hostname);
        return -1;
    }

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        close(sockfd);
        return -1;
    }

    return sockfd;
}

/* קריאת headers וחזרת מצביע לbody */
int read_headers(int sockfd, char *buffer, int buf_size,
                 int *status_code, long *content_length,
                 char *location, int loc_size, int *header_end)
{
    int total = 0;
    *content_length = -1;
    *status_code = 0;
    location[0] = '\0';

    while (total < buf_size - 1) {
        ssize_t n = read(sockfd, buffer + total, buf_size - total - 1);
        if (n <= 0) break;
        total += n;
        buffer[total] = '\0';

        char *sep = strstr(buffer, "\r\n\r\n");
        if (sep) {
            *header_end = (sep + 4) - buffer;

            /* פירסור status code */
            sscanf(buffer, "HTTP/%*s %d", status_code);

            /* פירסור Content-Length */
            char *cl = strstr(buffer, "Content-Length:");
            if (!cl) cl = strstr(buffer, "content-length:");
            if (cl) *content_length = strtol(cl + 15, NULL, 10);

            /* פירסור Location (לredirects) */
            char *loc = strstr(buffer, "Location:");
            if (!loc) loc = strstr(buffer, "location:");
            if (loc) {
                loc += 9;
                while (*loc == ' ') loc++;
                char *end = strstr(loc, "\r\n");
                if (end) {
                    int len = end - loc;
                    if (len >= loc_size) len = loc_size - 1;
                    memcpy(location, loc, len);
                    location[len] = '\0';
                }
            }

            return total;
        }
    }

    return total;
}

void print_progress(long downloaded, long total)
{
    if (total <= 0) {
        printf("\r%ld bytes downloaded...", downloaded);
    } else {
        int percent = (int)(downloaded * 100 / total);
        int bar_len = 30;
        int filled = bar_len * percent / 100;

        printf("\r[");
        for (int i = 0; i < bar_len; i++)
            printf("%c", i < filled ? '=' : ' ');
        printf("] %d%% (%ld/%ld bytes)", percent, downloaded, total);
    }
    fflush(stdout);
}

int download(const char *hostname, const char *path, const char *output,
             int redirect_count)
{
    if (redirect_count >= MAX_REDIRECTS) {
        fprintf(stderr, "too many redirects\n");
        return -1;
    }

    printf("downloading %s%s...\n", hostname, path);

    int sockfd = connect_to_host(hostname, 80);
    if (sockfd == -1) {
        fprintf(stderr, "cannot connect to %s\n", hostname);
        return -1;
    }

    /* שליחת בקשה */
    char request[1024];
    snprintf(request, sizeof(request),
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Connection: close\r\n"
        "User-Agent: Downloader/1.0\r\n"
        "\r\n",
        path, hostname);
    write(sockfd, request, strlen(request));

    /* קריאת headers */
    char buffer[BUFFER_SIZE];
    int status_code;
    long content_length;
    char location[512];
    int header_end;

    int total = read_headers(sockfd, buffer, sizeof(buffer),
                             &status_code, &content_length,
                             location, sizeof(location), &header_end);

    /* טיפול ב-redirect */
    if (status_code == 301 || status_code == 302) {
        close(sockfd);
        printf("redirect (%d) to: %s\n", status_code, location);

        /* פירסור הlocation - אם מתחיל ב-http://, חלץ hostname ו-path */
        char new_host[256], new_path[256];
        if (strncmp(location, "http://", 7) == 0) {
            char *host_start = location + 7;
            char *path_start = strchr(host_start, '/');
            if (path_start) {
                int host_len = path_start - host_start;
                memcpy(new_host, host_start, host_len);
                new_host[host_len] = '\0';
                strncpy(new_path, path_start, sizeof(new_path) - 1);
            } else {
                strncpy(new_host, host_start, sizeof(new_host) - 1);
                strcpy(new_path, "/");
            }
        } else {
            /* relative redirect */
            strncpy(new_host, hostname, sizeof(new_host) - 1);
            strncpy(new_path, location, sizeof(new_path) - 1);
        }

        return download(new_host, new_path, output, redirect_count + 1);
    }

    if (status_code != 200) {
        fprintf(stderr, "server returned %d\n", status_code);
        close(sockfd);
        return -1;
    }

    /* שמירה לקובץ */
    FILE *fp = fopen(output, "wb");
    if (!fp) {
        perror("fopen");
        close(sockfd);
        return -1;
    }

    /* כתיבת מה שכבר נקרא (אחרי הheaders) */
    long downloaded = total - header_end;
    if (downloaded > 0) {
        fwrite(buffer + header_end, 1, downloaded, fp);
    }
    print_progress(downloaded, content_length);

    /* קריאת שאר הbody */
    ssize_t n;
    while ((n = read(sockfd, buffer, sizeof(buffer))) > 0) {
        fwrite(buffer, 1, n, fp);
        downloaded += n;
        print_progress(downloaded, content_length);
    }

    printf("\n");
    fclose(fp);
    close(sockfd);

    printf("saved to %s (%ld bytes)\n", output, downloaded);
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc != 4) {
        fprintf(stderr, "usage: %s <hostname> <path> <output_file>\n", argv[0]);
        exit(1);
    }

    return download(argv[1], argv[2], argv[3], 0) == 0 ? 0 : 1;
}

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

  • הפונקציה read_headers קוראת עד שהיא מוצאת \r\n\r\n, ואז מפרסרת את הheaders הרלוונטיים. חלק מהbody עלול כבר להיות בבאפר - צריך לזכור לכתוב אותו לקובץ.
  • print_progress משתמשת ב-\r (carriage return) כדי לחזור לתחילת השורה ולעדכן את פס ההתקדמות בלי לרדת שורה.
  • טיפול ב-redirects: אם קוד המצב הוא 301 או 302, מחלצים את הכתובת החדשה מ-Location וקוראים ל-download רקורסיבית. מונה redirects מונע לולאה אינסופית.