Skip to content

Commit c6dd403

Browse files
feat:auto-index + redirection test
bugfix: check ERRNO for read in CGI handler applying format before checkout fixing redirection in SFH
1 parent 90fc979 commit c6dd403

File tree

10 files changed

+188
-34
lines changed

10 files changed

+188
-34
lines changed

config/site1.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ server {
55
index index.html index.htm;
66
allow_methods GET POST DELETE;
77

8+
location /old {
9+
return 302 /files;
10+
}
11+
812
location /pages {
13+
autoindex off;
914
}
1015

1116
location /images {
1217
}
1318

1419
location /files {
20+
autoindex on;
1521
}
1622
}

src/handler/cgi_handler.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ size_t CgiHandler::read_output(char* buf, size_t n)
322322
eof_reached_ = true;
323323
}
324324
else if (bytes_read < 0) {
325+
if (errno == EAGAIN || errno == EWOULDBLOCK) { // pipe not available for read
326+
return 0;
327+
}
325328
if (errno != EAGAIN && errno != EWOULDBLOCK) {
326329
if (!headers_parsed_) {
327330
set_res_and_quit(HttpResponse::kStatusBadGateway);

src/handler/redirect_handler.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ RedirectHandler::RedirectHandler(const RouteConfig& rc, const HttpRequest& req)
1111
req_(req),
1212
out_off_(0)
1313
{
14+
LOG(DEBUG) << "REDIRECTION HANDLER";
1415
res_ = HttpResponse::make_response_headers_only(rc_.shared.redirect.code, "", 0, req_);
1516
res_.location = rc_.shared.redirect.url;
1617
out_buf_ = res_.to_string();

src/handler/static_file_handler.cpp

Lines changed: 163 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,105 @@
55
#include "util/log_message.hpp"
66
#include "util/syscall_error.hpp"
77

8+
#include <dirent.h>
89
#include <errno.h>
910
#include <fcntl.h>
11+
#include <sys/stat.h>
1012
#include <unistd.h>
1113

14+
#include <algorithm>
1215
#include <cstring>
1316
#include <stdexcept>
1417
#include <vector>
1518

16-
static std::string resolve_path(const std::string& path,
17-
const std::vector<std::string>& index_files)
19+
static std::vector<std::string> list_dir(const std::string& dir_path)
1820
{
19-
struct stat sb;
20-
std::string full_path;
21-
std::string not_found = "";
21+
std::vector<std::string> names;
2222

23-
// Happy case: exact match found
24-
if (stat(path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode))
25-
return path;
23+
DIR* d = opendir(dir_path.c_str());
24+
if (!d)
25+
return names;
2626

27-
// Directory: check every index files
28-
if (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)) {
29-
for (size_t i = 0; i < index_files.size(); i++) {
30-
full_path = path + "/" + index_files[i];
31-
if (stat(full_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode))
32-
return full_path;
33-
}
34-
return not_found;
27+
for (dirent* ent = readdir(d); ent != NULL; ent = readdir(d)) {
28+
std::string name = ent->d_name;
29+
30+
if (name == "." || name == "..")
31+
continue;
32+
33+
names.push_back(name);
3534
}
35+
closedir(d);
3636

37-
// Clean URL fallback: check /foo -> /foo.html
38-
full_path = path + ".html";
39-
if (stat(full_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode))
40-
return full_path;
37+
std::sort(names.begin(), names.end());
38+
return names;
39+
}
40+
41+
static std::string html_escape(const std::string& s)
42+
{
43+
std::string out;
44+
out.reserve(s.size());
45+
for (size_t i = 0; i < s.size(); ++i) {
46+
char c = s[i];
47+
if (c == '&')
48+
out += "&amp;";
49+
else if (c == '<')
50+
out += "&lt;";
51+
else if (c == '>')
52+
out += "&gt;";
53+
else if (c == '"')
54+
out += "&quot;";
55+
else
56+
out += c;
57+
}
58+
return out;
59+
}
60+
61+
static std::string url_escape_min(const std::string& s)
62+
{
63+
std::string out;
64+
for (size_t i = 0; i < s.size(); ++i) {
65+
if (s[i] == ' ')
66+
out += "%20";
67+
else
68+
out += s[i];
69+
}
70+
return out;
71+
}
72+
73+
static std::string build_autoindex_html(const std::string& fs_dir, const std::string& url_dir)
74+
{
75+
std::vector<std::string> names = list_dir(fs_dir);
76+
77+
std::string body;
78+
body += "<!doctype html><html><head><meta charset=\"utf-8\">";
79+
body += "<title>Index of " + html_escape(url_dir) + "</title></head><body>";
80+
body += "<h1>Index of " + html_escape(url_dir) + "</h1>";
81+
body += "<ul>";
82+
83+
// Parent link (optional)
84+
if (url_dir != "/") {
85+
body += "<li><a href=\"../\">../</a></li>";
86+
}
87+
88+
for (size_t i = 0; i < names.size(); ++i) {
89+
const std::string& name = names[i];
4190

42-
return not_found;
91+
std::string fs_entry = fs_dir;
92+
if (!fs_entry.empty() && fs_entry[fs_entry.size() - 1] != '/')
93+
fs_entry += "/";
94+
fs_entry += name;
95+
96+
struct stat sb;
97+
bool is_dir = (stat(fs_entry.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
98+
99+
std::string display = html_escape(name) + (is_dir ? "/" : "");
100+
std::string href = url_escape_min(name) + (is_dir ? "/" : "");
101+
102+
body += "<li><a href=\"" + href + "\">" + display + "</a></li>";
103+
}
104+
105+
body += "</ul></body></html>";
106+
return body;
43107
}
44108

45109
static const char* derive_file_type(const std::string& file_path)
@@ -72,6 +136,45 @@ static const char* derive_file_type(const std::string& file_path)
72136
return "application/octet-stream";
73137
}
74138

139+
void StaticFileHandler::resolve_path(ResolveResult& resolve) const
140+
{
141+
struct stat sb;
142+
std::string full_path;
143+
const std::vector<std::string>& index_files = rc_.shared.index_files;
144+
// Happy case: exact match found
145+
if (stat(path_.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
146+
resolve.path = path_;
147+
resolve.kind = kFile;
148+
return;
149+
}
150+
// Directory: check every index files
151+
if (stat(path_.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)) {
152+
for (size_t i = 0; i < index_files.size(); i++) {
153+
full_path = path_ + "/" + index_files[i];
154+
if (stat(full_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
155+
resolve.path = full_path;
156+
resolve.kind = kFile;
157+
return;
158+
}
159+
}
160+
resolve.path = path_;
161+
resolve.kind = kDirNoIndex;
162+
return;
163+
}
164+
165+
// Clean URL fallback: check /foo -> /foo.html
166+
full_path = path_ + ".html";
167+
if (stat(full_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
168+
resolve.path = full_path;
169+
resolve.kind = kFile;
170+
return;
171+
}
172+
173+
resolve.path = "";
174+
resolve.kind = kNotFound;
175+
return;
176+
}
177+
75178
StaticFileHandler::StaticFileHandler(const std::string& path,
76179
const RouteConfig& rc,
77180
const HttpRequest& req)
@@ -83,24 +186,40 @@ StaticFileHandler::StaticFileHandler(const std::string& path,
83186
fd_(-1)
84187

85188
{
86-
87-
if (rc.shared.index_files.empty())
88-
LOG(WARN) << "Index files are empty";
89-
std::string full_path = resolve_path(path, rc.shared.index_files);
90-
if (full_path.empty()) {
91-
LOG(ERROR) << "Couldn't open file " << path;
189+
LOG(DEBUG) << "req_.path = " << req_.path;
190+
struct ResolveResult r;
191+
resolve_path(r);
192+
if (r.kind == kNotFound) {
92193
set_error(HttpResponse::kStatusNotFound);
93194
return;
94195
}
196+
if (r.kind == kDirNoIndex && !rc_.shared.autoindex_enabled) {
197+
set_error(HttpResponse::kStatusForbidden);
198+
return;
199+
}
200+
if (r.kind == kDirNoIndex && rc_.shared.autoindex_enabled && r.path[r.path.size() - 1] != '/') {
201+
set_redirect(HttpResponse::kStatusMovedPermanently, req_.path + "/");
202+
return;
203+
}
204+
if (r.kind == kDirNoIndex && rc_.shared.autoindex_enabled) {
205+
std::string html = build_autoindex_html(r.path, req_.path);
206+
res_ = HttpResponse::make_response_with_body(
207+
HttpResponse::kStatusOk, "text/html; charset=UTF-8", html, req_);
208+
209+
out_buf_ = res_.to_string();
210+
fd_ = -1;
211+
return;
212+
}
213+
95214
struct stat file_stat;
96-
fd_ = open(full_path.data(), O_RDONLY);
215+
fd_ = open(r.path.data(), O_RDONLY);
97216
if (fd_ == -1) {
98217
set_error(HttpResponse::kStatusInternalServerError);
99218
return;
100219
}
101-
stat(full_path.data(), &file_stat);
220+
stat(r.path.data(), &file_stat);
102221
file_size_ = file_stat.st_size;
103-
std::string file_type = derive_file_type(full_path);
222+
std::string file_type = derive_file_type(r.path);
104223
res_ = HttpResponse::make_response_headers_only(
105224
HttpResponse::kStatusOk, file_type, file_size_, req_);
106225
out_buf_ = res_.to_string();
@@ -152,4 +271,18 @@ void StaticFileHandler::set_error(const HttpResponse::Status code)
152271
{
153272
res_ = HttpResponse::make_error(code, rc_.shared.error_pages, req_);
154273
out_buf_ = res_.to_string();
274+
if (fd_ != -1) {
275+
close(fd_);
276+
fd_ = -1;
277+
}
278+
}
279+
280+
void StaticFileHandler::set_redirect(const HttpResponse::Status code,
281+
const std::string& redirect_path)
282+
{
283+
res_ = HttpResponse::make_response_headers_only(code, "", 0, req_);
284+
// LOG(DEBUG) << "redirect location = " << redirect_path;
285+
res_.location = redirect_path;
286+
out_buf_ = res_.to_string();
287+
fd_ = -1;
155288
}

src/handler/static_file_handler.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
#include <string>
1111

12+
enum ResolveKind { kFile, kDirNoIndex, kNotFound };
13+
14+
struct ResolveResult {
15+
ResolveKind kind;
16+
std::string path;
17+
};
18+
1219
class StaticFileHandler : public Handler {
1320
public:
1421
StaticFileHandler(const std::string& path, const RouteConfig& rc, const HttpRequest& req);
@@ -29,7 +36,9 @@ class StaticFileHandler : public Handler {
2936
private:
3037
StaticFileHandler(const StaticFileHandler&);
3138
StaticFileHandler& operator=(const StaticFileHandler&);
39+
void resolve_path(ResolveResult& resolve) const;
3240
void set_error(const HttpResponse::Status code);
41+
void set_redirect(const HttpResponse::Status code, const std::string& redirect_path);
3342

3443
// constructor args
3544
const std::string& path_;

src/http/http_response.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ std::string HttpResponse::to_string() const
103103
if (!inline_body.empty()) {
104104
out << "Content-Length: " << inline_body.size() << "\r\n";
105105
}
106-
else if (content_length > 0) {
106+
else {
107107
out << "Content-Length: " << content_length << "\r\n";
108108
}
109109
if (is_chunked) {
@@ -165,7 +165,7 @@ HttpResponse HttpResponse::make_response_with_body(HttpResponse::Status status,
165165
res.code = status;
166166
res.content_type = content_type;
167167
res.keep_alive = req.keep_alive;
168-
if (status == 204 || status == 304) {
168+
if (status == kStatusNoContent || status == 304) {
169169
res.inline_body = "";
170170
res.content_length = 0;
171171
}

src/main.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ int main(void)
1010
setup_signal_handlers();
1111

1212
try {
13-
HttpConfig cfg = load_http_config("config/vitepress.conf");
13+
// HttpConfig cfg = load_http_config("config/vitepress.conf");
14+
HttpConfig cfg = load_http_config("config/site1.conf");
1415

1516
Server server(cfg.servers[0]);
1617
server.init();

tests/redirect_handler_unittest.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ UTEST(RedirectHandlerTest, builds_redirect_response_with_location_header)
5252
ASSERT_TRUE(is_301);
5353

5454
// Usually redirects have no body in your implementation
55-
ASSERT_TRUE(resp.find("Content-Length: 0") == std::string::npos);
55+
ASSERT_TRUE(resp.find("Content-Length: 0") != std::string::npos);
5656

5757
// Must end headers correctly (at least one empty line)
5858
ASSERT_TRUE(resp.find("\r\n\r\n") != std::string::npos ||

www/site1/files/second_file.txt

Whitespace-only changes.

www/site1/pages/someOtherSite.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>someContent</h1>

0 commit comments

Comments
 (0)