<think>我们之前设计的HTTP服务器中,文件路径是直接使用当前目录(`.`)作为根目录。现在要求将文件放在当前工作目录的`web`文件夹内,我们需要修改文件路径的构建方式。
修改点:
1. 在构建实际文件路径时,将基础目录改为`./web`,然后加上URI路径。
2. 需要确保路径安全,防止路径遍历攻击(例如使用`..`跳出web目录)。
具体修改步骤:
在`Handle_Request_Message`函数中,修改构建文件路径的代码:
原代码:`snprintf(path, BUF_SIZE, ".%s", uri);`
修改为:`snprintf(path, BUF_SIZE, "./web%s", uri);`
但是,这样修改后,我们还需要确保:
- 如果URI是`/`,则实际路径为`./web/index.html`(或指定的默认页面)
- 防止路径遍历:通过检查最终路径是否在`./web`目录内。
安全增强:我们可以使用`realpath`函数来解析绝对路径,并检查是否在web目录内。
修改后的代码逻辑:
1. 构建基础目录:`base_dir = "./web"`
2. 将URI附加到基础目录:`sprintf(full_path, "%s%s", base_dir, uri)`
3. 使用`realpath`解析完整路径,并检查是否以基础目录的绝对路径开头。
注意:使用`realpath`需要确保路径存在,否则返回NULL。
修改后的`Handle_Request_Message`函数中关于路径处理的部分:
步骤:
a. 定义基础目录
b. 构建完整路径(相对路径)
c. 解析绝对路径并验证
代码示例:
```c
// ... 之前的代码
// 3. 处理URI
// 根路径重定向
if (strcmp(uri, "/") == 0) {
strncpy(uri, "/index.html", BUF_SIZE - 1);
}
// 构建基础目录
const char *base_dir = "./web";
char full_path[BUF_SIZE];
snprintf(full_path, BUF_SIZE, "%s%s", base_dir, uri);
// 防止路径遍历攻击:使用realpath检查
char resolved_path[BUF_SIZE];
char base_abs_path[BUF_SIZE];
// 获取基础目录的绝对路径
if (realpath(base_dir, base_abs_path) == NULL) {
perror("realpath base_dir");
Send_Error_Response(socket, 500, "Internal Server Error");
return ERROR;
}
// 获取请求文件的绝对路径
if (realpath(full_path, resolved_path) == NULL) {
// 文件不存在或路径无效
Send_Error_Response(socket, 404, "Not Found");
return ERROR;
}
// 检查解析后的路径是否在基础目录内
if (strncmp(resolved_path, base_abs_path, strlen(base_abs_path)) != 0) {
// 不在基础目录内,可能是路径遍历攻击
Send_Error_Response(socket, 403, "Forbidden");
return ERROR;
}
// 现在使用resolved_path作为文件路径
// ... 后续的stat和Send_File都使用resolved_path
```
注意:使用`realpath`函数需要包含头文件`<stdlib.h>`和`<limits.h>`。
完整修改后的`Handle_Request_Message`函数(只显示修改部分):
```c
#include <stdlib.h>
#include <limits.h>
int Handle_Request_Message(char *request, int socket) {
// ... 之前解析请求行、方法等
// 3. 处理URI
if (strcmp(uri, "/") == 0) {
strncpy(uri, "/index.html", BUF_SIZE - 1);
}
// 设置基础目录
const char *base_dir = "./web";
char full_path[BUF_SIZE];
snprintf(full_path, BUF_SIZE, "%s%s", base_dir, uri);
// 安全验证:获取绝对路径并检查是否在基础目录下
char base_abs_path[BUF_SIZE];
if (realpath(base_dir, base_abs_path) == NULL) {
Send_Error_Response(socket, 500, "Internal Server Error");
return ERROR;
}
char resolved_path[BUF_SIZE];
if (realpath(full_path, resolved_path) == NULL) {
Send_Error_Response(socket, 404, "Not Found");
return ERROR;
}
// 检查解析后的路径是否在base_abs_path目录下
size_t base_len = strlen(base_abs_path);
if (strncmp(resolved_path, base_abs_path, base_len) != 0) {
Send_Error_Response(socket, 403, "Forbidden");
return ERROR;
}
// 检查文件是否存在且为普通文件
struct stat st;
if (stat(resolved_path, &st) == -1 || !S_ISREG(st.st_mode)) {
Send_Error_Response(socket, 404, "Not Found");
return ERROR;
}
// 5. 发送响应(使用resolved_path)
if (Send_File(resolved_path, socket, method_type) != OK) {
Send_Error_Response(socket, 500, "Internal Server Error");
return ERROR;
}
return OK;
}
```
注意:这里使用了`realpath`来解析绝对路径,并检查文件是否在基础目录内。这样可以有效防止路径遍历攻击。
另外,如果基础目录`./web`不存在,`realpath`会返回NULL,所以服务器启动时应确保该目录存在。
关于`realpath`函数:
- 功能:将相对路径转换为绝对路径,解析其中的`/./`、`/../`以及多余的`/`。
- 返回值:成功时返回绝对路径字符串指针(存放在resolved_path中),失败返回NULL(错误原因在errno中)。
这种安全措施可以确保用户无法通过`../`跳出web目录。
修改后,所有的文件都将在`./web`目录下提供。
### §§相关问题§§:
1. 在C语言中,`realpath`函数的工作原理是什么?它如何处理符号链接?
2. 除了使用`realpath`,还有哪些方法可以防止路径遍历攻击?
3. 在HTTP服务器中,如何处理对目录的请求(例如URI以`/`结尾)?
4. 如何实现一个目录列表(directory listing)功能,当请求目录时显示文件列表?</think>### 文件存储在 `web` 文件夹的实现方案
当文件需要存储在 `web` 文件夹内时,我们需要修改文件路径处理逻辑,并确保所有文件访问都限制在该目录下。以下是完整的实现方案:
#### 核心修改点
1. **设置根目录常量**
2. **路径规范化处理**
3. **安全防护增强**
4. **目录请求处理**
### 完整实现代码
```c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#define BUF_SIZE 4096
#define WEB_ROOT "./web" // 定义web根目录
// 文件发送函数(基于之前实现的Send_File)
int Send_File(const char *path, int socket, int method);
// 错误响应函数
int Send_Error_Response(int socket, int status_code, const char *message) {
char response[BUF_SIZE];
int length = snprintf(response, BUF_SIZE,
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"Error %d: %s",
status_code, message, status_code, message);
send(socket, response, length, 0);
return 0;
}
// 检查路径是否在web目录内
int is_path_safe(const char *path) {
char resolved_path[BUF_SIZE];
// 获取绝对路径
if (realpath(path, resolved_path) == NULL) {
return 0;
}
// 检查是否在WEB_ROOT目录下
return strncmp(resolved_path, realpath(WEB_ROOT, NULL), strlen(realpath(WEB_ROOT, NULL))) == 0;
}
// 处理目录请求
int handle_directory_request(const char *path, int socket) {
char index_path[BUF_SIZE];
snprintf(index_path, BUF_SIZE, "%s/index.html", path);
// 检查是否存在index.html
struct stat st;
if (stat(index_path, &st) == 0 && S_ISREG(st.st_mode)) {
return Send_File(index_path, socket, GET_METHOD);
}
// 没有index.html,返回目录列表
DIR *dir = opendir(path);
if (!dir) {
Send_Error_Response(socket, 403, "Forbidden");
return 0;
}
// 生成目录列表HTML
char response[BUF_SIZE * 2];
int len = snprintf(response, BUF_SIZE * 2,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html><head><title>Directory Listing</title></head><body>"
"<h1>Directory Listing: %s</h1><ul>", path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 跳过隐藏文件和当前目录
if (entry->d_name[0] == '.' || strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
char entry_path[BUF_SIZE];
snprintf(entry_path, BUF_SIZE, "%s/%s", path, entry->d_name);
struct stat entry_st;
if (stat(entry_path, &entry_st) != 0) continue;
const char *type = S_ISDIR(entry_st.st_mode) ? "📁" : "📄";
len += snprintf(response + len, (BUF_SIZE * 2) - len,
"<li>%s <a href=\"%s\">%s</a> (%ld bytes)</li>",
type, entry->d_name, entry->d_name, entry_st.st_size);
}
len += snprintf(response + len, (BUF_SIZE * 2) - len, "</ul></body></html>");
closedir(dir);
send(socket, response, len, 0);
return 1;
}
// 修改后的请求处理函数
int Handle_Request_Message(char *request, int socket) {
char method[16], uri[BUF_SIZE], version[16];
// 解析请求行
if (sscanf(request, "%15s %1023s %15s", method, uri, version) != 3) {
Send_Error_Response(socket, 400, "Bad Request");
return 0;
}
// 构建完整文件路径
char path[BUF_SIZE];
snprintf(path, BUF_SIZE, "%s%s", WEB_ROOT, uri);
// 处理目录请求(以/结尾)
size_t len = strlen(path);
if (len > 0 && path[len-1] == '/') {
return handle_directory_request(path, socket);
}
// 检查路径安全性
if (!is_path_safe(path)) {
Send_Error_Response(socket, 403, "Forbidden");
return 0;
}
// 检查文件是否存在
struct stat st;
if (stat(path, &st) == -1) {
// 尝试添加.html扩展名
if (strstr(path, ".") == NULL) {
char html_path[BUF_SIZE];
snprintf(html_path, BUF_SIZE, "%s.html", path);
if (stat(html_path, &st) == 0) {
strcpy(path, html_path);
} else {
Send_Error_Response(socket, 404, "Not Found");
return 0;
}
} else {
Send_Error_Response(socket, 404, "Not Found");
return 0;
}
}
// 处理目录请求(没有结尾斜杠)
if (S_ISDIR(st.st_mode)) {
// 重定向到带斜杠的URL
char redirect[BUF_SIZE];
snprintf(redirect, BUF_SIZE, "HTTP/1.1 301 Moved Permanently\r\n"
"Location: %s/\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n\r\n", uri);
send(socket, redirect, strlen(redirect), 0);
return 1;
}
// 发送文件
return Send_File(path, socket, GET_METHOD);
}
// 修改后的Send_File函数
int Send_File(const char *path, int socket, int method) {
int fd = open(path, O_RDONLY);
if (fd == -1) {
Send_Error_Response(socket, 500, "Internal Server Error");
return 0;
}
struct stat file_stat;
if (fstat(fd, &file_stat) == -1) {
close(fd);
Send_Error_Response(socket, 500, "Internal Server Error");
return 0;
}
// 获取内容类型
const char *content_type = "text/plain";
const char *ext = strrchr(path, '.');
if (ext) {
if (strcmp(ext, ".html") == 0) content_type = "text/html";
else if (strcmp(ext, ".css") == 0) content_type = "text/css";
else if (strcmp(ext, ".js") == 0) content_type = "application/javascript";
else if (strcmp(ext, ".jpg") == 0) content_type = "image/jpeg";
else if (strcmp(ext, ".png") == 0) content_type = "image/png";
}
// 发送HTTP头
char headers[BUF_SIZE];
int headers_len = snprintf(headers, BUF_SIZE,
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n\r\n",
content_type, file_stat.st_size);
send(socket, headers, headers_len, 0);
// 发送文件内容
char buffer[BUF_SIZE];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, BUF_SIZE)) > 0) {
if (send(socket, buffer, bytes_read, 0) != bytes_read) {
break;
}
}
close(fd);
return 1;
}
```
### 关键安全增强措施
#### 1. 路径安全验证
```c
int is_path_safe(const char *path) {
char resolved_path[BUF_SIZE];
if (realpath(path, resolved_path) == NULL) {
return 0;
}
// 获取web目录的绝对路径
char web_abs_path[BUF_SIZE];
realpath(WEB_ROOT, web_abs_path);
// 检查解析后的路径是否在web目录内
return strncmp(resolved_path, web_abs_path, strlen(web_abs_path)) == 0;
}
```
- **原理**:使用`realpath()`解析绝对路径
- **作用**:防止`../`路径遍历攻击
- **验证**:确保文件路径以web根目录开头
#### 2. 目录请求处理
```c
// 处理以斜杠结尾的URL
if (len > 0 && path[len-1] == '/') {
return handle_directory_request(path, socket);
}
// 处理没有斜杠的目录请求
if (S_ISDIR(st.st_mode)) {
// 301重定向到带斜杠的URL
char redirect[BUF_SIZE];
snprintf(redirect, BUF_SIZE, "HTTP/1.1 301 Moved Permanently\r\n"
"Location: %s/\r\n", uri);
send(socket, redirect, strlen(redirect), 0);
return 1;
}
```
- **目录斜杠处理**:确保目录URL始终以`/`结尾
- **重定向**:301永久重定向保持SEO友好
- **目录列表**:当不存在index.html时显示目录内容
#### 3. 文件扩展名处理
```c
// 检查文件是否存在
if (stat(path, &st) == -1) {
// 尝试添加.html扩展名
if (strstr(path, ".") == NULL) {
char html_path[BUF_SIZE];
snprintf(html_path, BUF_SIZE, "%s.html", path);
if (stat(html_path, &st) == 0) {
strcpy(path, html_path);
}
}
}
```
- **无扩展名处理**:自动尝试添加`.html`扩展名
- **用户友好**:支持`/about`访问`/about.html`
- **安全**:仅当原始路径无扩展名时尝试
### 目录列表功能实现
```c
int handle_directory_request(const char *path, int socket) {
// 检查index.html是否存在
char index_path[BUF_SIZE];
snprintf(index_path, BUF_SIZE, "%s/index.html", path);
if (access(index_path, F_OK) == 0) {
return Send_File(index_path, socket, GET_METHOD);
}
// 生成目录列表
DIR *dir = opendir(path);
if (!dir) return 0;
char html[BUF_SIZE * 2];
int len = snprintf(html, BUF_SIZE * 2,
"<html><head><title>Index of %s</title></head><body>"
"<h1>Index of %s</h1><table>", path, path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 跳过特殊目录
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
char full_path[BUF_SIZE];
snprintf(full_path, BUF_SIZE, "%s/%s", path, entry->d_name);
struct stat entry_stat;
stat(full_path, &entry_stat);
const char *icon = S_ISDIR(entry_stat.st_mode) ? "📁" : "📄";
len += snprintf(html + len, (BUF_SIZE * 2) - len,
"<tr><td>%s</td><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
icon, entry->d_name, entry->d_name, entry_stat.st_size);
}
len += snprintf(html + len, (BUF_SIZE * 2) - len, "</table></body></html>");
closedir(dir);
// 发送HTML响应
char headers[BUF_SIZE];
int headers_len = snprintf(headers, BUF_SIZE,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %d\r\n\r\n", len);
send(socket, headers, headers_len, 0);
send(socket, html, len, 0);
return 1;
}
```
### 部署建议
1. **目录结构示例**
```
项目目录/
├── server.c # 服务器源代码
├── web/ # WEB根目录
│ ├── index.html # 默认首页
│ ├── about.html
│ ├── styles/
│ │ └── main.css
│ ├── images/
│ │ └── logo.png
│ └── downloads/
│ └── manual.pdf
```
2. **启动服务器前准备**
```bash
# 创建web目录和内容
mkdir -p web/{styles,images,downloads}
echo "<h1>Welcome</h1>" > web/index.html
echo "body { color: blue; }" > web/styles/main.css
# 编译并运行服务器
gcc server.c -o server
./server
```
3. **访问示例**
- `http://localhost:8080/` → 显示index.html
- `http://localhost:8080/about` → 显示about.html
- `http://localhost:8080/styles/` → 显示styles目录内容
- `http://localhost:8080/images/logo.png` → 下载图片
###