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 מונע לולאה אינסופית.