1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/http
7  
// Official repository: https://github.com/cppalliance/http
8  
//
8  
//
9  

9  

10  
#include <boost/http/server/serve_static.hpp>
10  
#include <boost/http/server/serve_static.hpp>
11  
#include <boost/http/server/send_file.hpp>
11  
#include <boost/http/server/send_file.hpp>
12  
#include <boost/http/field.hpp>
12  
#include <boost/http/field.hpp>
13  
#include <boost/http/file.hpp>
13  
#include <boost/http/file.hpp>
14  
#include <boost/http/status.hpp>
14  
#include <boost/http/status.hpp>
15  
#include <filesystem>
15  
#include <filesystem>
16  
#include <string>
16  
#include <string>
17  

17  

18  
namespace boost {
18  
namespace boost {
19  
namespace http {
19  
namespace http {
20  

20  

21  
namespace {
21  
namespace {
22  

22  

23  
// Append an HTTP rel-path to a local filesystem path.
23  
// Append an HTTP rel-path to a local filesystem path.
24  
void
24  
void
25  
path_cat(
25  
path_cat(
26  
    std::string& result,
26  
    std::string& result,
27  
    core::string_view prefix,
27  
    core::string_view prefix,
28  
    core::string_view suffix)
28  
    core::string_view suffix)
29  
{
29  
{
30  
    result = prefix;
30  
    result = prefix;
31  

31  

32  
#ifdef BOOST_MSVC
32  
#ifdef BOOST_MSVC
33  
    char constexpr path_separator = '\\';
33  
    char constexpr path_separator = '\\';
34  
#else
34  
#else
35  
    char constexpr path_separator = '/';
35  
    char constexpr path_separator = '/';
36  
#endif
36  
#endif
37  
    if(! result.empty() && result.back() == path_separator)
37  
    if(! result.empty() && result.back() == path_separator)
38  
        result.resize(result.size() - 1);
38  
        result.resize(result.size() - 1);
39  

39  

40  
#ifdef BOOST_MSVC
40  
#ifdef BOOST_MSVC
41  
    for(auto& c : result)
41  
    for(auto& c : result)
42  
        if(c == '/')
42  
        if(c == '/')
43  
            c = path_separator;
43  
            c = path_separator;
44  
#endif
44  
#endif
45  
    for(auto const& c : suffix)
45  
    for(auto const& c : suffix)
46  
    {
46  
    {
47  
        if(c == '/')
47  
        if(c == '/')
48  
            result.push_back(path_separator);
48  
            result.push_back(path_separator);
49  
        else
49  
        else
50  
            result.push_back(c);
50  
            result.push_back(c);
51  
    }
51  
    }
52  
}
52  
}
53  

53  

54  
// Check if path segment is a dotfile
54  
// Check if path segment is a dotfile
55  
bool
55  
bool
56  
is_dotfile(core::string_view path) noexcept
56  
is_dotfile(core::string_view path) noexcept
57  
{
57  
{
58  
    auto pos = path.rfind('/');
58  
    auto pos = path.rfind('/');
59  
    if(pos == core::string_view::npos)
59  
    if(pos == core::string_view::npos)
60  
        pos = 0;
60  
        pos = 0;
61  
    else
61  
    else
62  
        ++pos;
62  
        ++pos;
63  

63  

64  
    if(pos < path.size() && path[pos] == '.')
64  
    if(pos < path.size() && path[pos] == '.')
65  
        return true;
65  
        return true;
66  

66  

67  
    return false;
67  
    return false;
68  
}
68  
}
69  

69  

70  
} // (anon)
70  
} // (anon)
71  

71  

72  
struct serve_static::impl
72  
struct serve_static::impl
73  
{
73  
{
74  
    std::string root;
74  
    std::string root;
75  
    serve_static_options opts;
75  
    serve_static_options opts;
76  

76  

77  
    impl(
77  
    impl(
78  
        core::string_view root_,
78  
        core::string_view root_,
79  
        serve_static_options const& opts_)
79  
        serve_static_options const& opts_)
80  
        : root(root_)
80  
        : root(root_)
81  
        , opts(opts_)
81  
        , opts(opts_)
82  
    {
82  
    {
83  
    }
83  
    }
84  
};
84  
};
85  

85  

86  
serve_static::
86  
serve_static::
87  
~serve_static()
87  
~serve_static()
88  
{
88  
{
89  
    delete impl_;
89  
    delete impl_;
90  
}
90  
}
91  

91  

92  
serve_static::
92  
serve_static::
93  
serve_static(core::string_view root)
93  
serve_static(core::string_view root)
94  
    : serve_static(root, serve_static_options{})
94  
    : serve_static(root, serve_static_options{})
95  
{
95  
{
96  
}
96  
}
97  

97  

98  
serve_static::
98  
serve_static::
99  
serve_static(
99  
serve_static(
100  
    core::string_view root,
100  
    core::string_view root,
101  
    serve_static_options const& opts)
101  
    serve_static_options const& opts)
102  
    : impl_(new impl(root, opts))
102  
    : impl_(new impl(root, opts))
103  
{
103  
{
104  
}
104  
}
105  

105  

106  
serve_static::
106  
serve_static::
107  
serve_static(serve_static&& other) noexcept
107  
serve_static(serve_static&& other) noexcept
108  
    : impl_(other.impl_)
108  
    : impl_(other.impl_)
109  
{
109  
{
110  
    other.impl_ = nullptr;
110  
    other.impl_ = nullptr;
111  
}
111  
}
112  

112  

113  
route_task
113  
route_task
114  
serve_static::
114  
serve_static::
115  
operator()(route_params& rp) const
115  
operator()(route_params& rp) const
116  
{
116  
{
117  
    // Only handle GET and HEAD
117  
    // Only handle GET and HEAD
118  
    if(rp.req.method() != method::get &&
118  
    if(rp.req.method() != method::get &&
119  
        rp.req.method() != method::head)
119  
        rp.req.method() != method::head)
120  
    {
120  
    {
121  
        if(impl_->opts.fallthrough)
121  
        if(impl_->opts.fallthrough)
122  
            co_return route_next;
122  
            co_return route_next;
123  

123  

124  
        rp.res.set_status(status::method_not_allowed);
124  
        rp.res.set_status(status::method_not_allowed);
125  
        rp.res.set(field::allow, "GET, HEAD");
125  
        rp.res.set(field::allow, "GET, HEAD");
126  
        auto [ec] = co_await rp.send();
126  
        auto [ec] = co_await rp.send();
127  
        if(ec)
127  
        if(ec)
128  
            co_return route_error(ec);
128  
            co_return route_error(ec);
129  
        co_return route_done;
129  
        co_return route_done;
130  
    }
130  
    }
131  

131  

132  
    // Get the request path
132  
    // Get the request path
133  
    auto req_path = rp.url.path();
133  
    auto req_path = rp.url.path();
134  

134  

135  
    // Check for dotfiles
135  
    // Check for dotfiles
136  
    if(is_dotfile(req_path))
136  
    if(is_dotfile(req_path))
137  
    {
137  
    {
138  
        switch(impl_->opts.dotfiles)
138  
        switch(impl_->opts.dotfiles)
139  
        {
139  
        {
140  
        case dotfiles_policy::deny:
140  
        case dotfiles_policy::deny:
141  
        {
141  
        {
142  
            rp.res.set_status(status::forbidden);
142  
            rp.res.set_status(status::forbidden);
143  
            auto [ec] = co_await rp.send("Forbidden");
143  
            auto [ec] = co_await rp.send("Forbidden");
144  
            if(ec)
144  
            if(ec)
145  
                co_return route_error(ec);
145  
                co_return route_error(ec);
146  
            co_return route_done;
146  
            co_return route_done;
147  
        }
147  
        }
148  

148  

149  
        case dotfiles_policy::ignore:
149  
        case dotfiles_policy::ignore:
150  
        {
150  
        {
151  
            if(impl_->opts.fallthrough)
151  
            if(impl_->opts.fallthrough)
152  
                co_return route_next;
152  
                co_return route_next;
153  
            rp.res.set_status(status::not_found);
153  
            rp.res.set_status(status::not_found);
154  
            auto [ec] = co_await rp.send("Not Found");
154  
            auto [ec] = co_await rp.send("Not Found");
155  
            if(ec)
155  
            if(ec)
156  
                co_return route_error(ec);
156  
                co_return route_error(ec);
157  
            co_return route_done;
157  
            co_return route_done;
158  
        }
158  
        }
159  

159  

160  
        case dotfiles_policy::allow:
160  
        case dotfiles_policy::allow:
161  
            break;
161  
            break;
162  
        }
162  
        }
163  
    }
163  
    }
164  

164  

165  
    // Build the file path
165  
    // Build the file path
166  
    std::string path;
166  
    std::string path;
167  
    path_cat(path, impl_->root, req_path);
167  
    path_cat(path, impl_->root, req_path);
168  

168  

169  
    // Check if it's a directory
169  
    // Check if it's a directory
170  
    system::error_code fec;
170  
    system::error_code fec;
171  
    bool is_dir = std::filesystem::is_directory(path, fec);
171  
    bool is_dir = std::filesystem::is_directory(path, fec);
172  
    if(is_dir && ! fec.failed())
172  
    if(is_dir && ! fec.failed())
173  
    {
173  
    {
174  
        // Check for trailing slash
174  
        // Check for trailing slash
175  
        if(req_path.empty() || req_path.back() != '/')
175  
        if(req_path.empty() || req_path.back() != '/')
176  
        {
176  
        {
177  
            if(impl_->opts.redirect)
177  
            if(impl_->opts.redirect)
178  
            {
178  
            {
179  
                // Redirect to add trailing slash
179  
                // Redirect to add trailing slash
180  
                std::string location(req_path);
180  
                std::string location(req_path);
181  
                location += '/';
181  
                location += '/';
182  
                rp.res.set_status(status::moved_permanently);
182  
                rp.res.set_status(status::moved_permanently);
183  
                rp.res.set(field::location, location);
183  
                rp.res.set(field::location, location);
184  
                auto [ec] = co_await rp.send("");
184  
                auto [ec] = co_await rp.send("");
185  
                if(ec)
185  
                if(ec)
186  
                    co_return route_error(ec);
186  
                    co_return route_error(ec);
187  
                co_return route_done;
187  
                co_return route_done;
188  
            }
188  
            }
189  
        }
189  
        }
190  

190  

191  
        // Try index file
191  
        // Try index file
192  
        if(impl_->opts.index)
192  
        if(impl_->opts.index)
193  
        {
193  
        {
194  
#ifdef BOOST_MSVC
194  
#ifdef BOOST_MSVC
195  
            path += "\\index.html";
195  
            path += "\\index.html";
196  
#else
196  
#else
197  
            path += "/index.html";
197  
            path += "/index.html";
198  
#endif
198  
#endif
199  
        }
199  
        }
200  
    }
200  
    }
201  

201  

202  
    // Prepare file response using send_file utilities
202  
    // Prepare file response using send_file utilities
203  
    send_file_options opts;
203  
    send_file_options opts;
204  
    opts.etag = impl_->opts.etag;
204  
    opts.etag = impl_->opts.etag;
205  
    opts.last_modified = impl_->opts.last_modified;
205  
    opts.last_modified = impl_->opts.last_modified;
206  
    opts.max_age = impl_->opts.max_age;
206  
    opts.max_age = impl_->opts.max_age;
207  

207  

208  
    send_file_info info;
208  
    send_file_info info;
209  
    send_file_init(info, rp, path, opts);
209  
    send_file_init(info, rp, path, opts);
210  

210  

211  
    // Handle result
211  
    // Handle result
212  
    switch(info.result)
212  
    switch(info.result)
213  
    {
213  
    {
214  
    case send_file_result::not_found:
214  
    case send_file_result::not_found:
215  
    {
215  
    {
216  
        if(impl_->opts.fallthrough)
216  
        if(impl_->opts.fallthrough)
217  
            co_return route_next;
217  
            co_return route_next;
218  
        rp.res.set_status(status::not_found);
218  
        rp.res.set_status(status::not_found);
219  
        auto [ec] = co_await rp.send("Not Found");
219  
        auto [ec] = co_await rp.send("Not Found");
220  
        if(ec)
220  
        if(ec)
221  
            co_return route_error(ec);
221  
            co_return route_error(ec);
222  
        co_return route_done;
222  
        co_return route_done;
223  
    }
223  
    }
224  

224  

225  
    case send_file_result::not_modified:
225  
    case send_file_result::not_modified:
226  
    {
226  
    {
227  
        rp.res.set_status(status::not_modified);
227  
        rp.res.set_status(status::not_modified);
228  
        auto [ec] = co_await rp.send("");
228  
        auto [ec] = co_await rp.send("");
229  
        if(ec)
229  
        if(ec)
230  
            co_return route_error(ec);
230  
            co_return route_error(ec);
231  
        co_return route_done;
231  
        co_return route_done;
232  
    }
232  
    }
233  

233  

234  
    case send_file_result::error:
234  
    case send_file_result::error:
235  
    {
235  
    {
236  
        // Range error - headers already set by send_file_init
236  
        // Range error - headers already set by send_file_init
237  
        auto [ec] = co_await rp.send("");
237  
        auto [ec] = co_await rp.send("");
238  
        if(ec)
238  
        if(ec)
239  
            co_return route_error(ec);
239  
            co_return route_error(ec);
240  
        co_return route_done;
240  
        co_return route_done;
241  
    }
241  
    }
242  

242  

243  
    case send_file_result::ok:
243  
    case send_file_result::ok:
244  
        break;
244  
        break;
245  
    }
245  
    }
246  

246  

247  
    // Set Accept-Ranges if enabled
247  
    // Set Accept-Ranges if enabled
248  
    if(impl_->opts.accept_ranges)
248  
    if(impl_->opts.accept_ranges)
249  
        rp.res.set(field::accept_ranges, "bytes");
249  
        rp.res.set(field::accept_ranges, "bytes");
250  

250  

251  
    // Set Cache-Control with immutable if configured
251  
    // Set Cache-Control with immutable if configured
252  
    if(impl_->opts.immutable && opts.max_age > 0)
252  
    if(impl_->opts.immutable && opts.max_age > 0)
253  
    {
253  
    {
254  
        std::string cc = "public, max-age=" +
254  
        std::string cc = "public, max-age=" +
255  
            std::to_string(opts.max_age) + ", immutable";
255  
            std::to_string(opts.max_age) + ", immutable";
256  
        rp.res.set(field::cache_control, cc);
256  
        rp.res.set(field::cache_control, cc);
257  
    }
257  
    }
258  

258  

259  
    // For HEAD requests, don't send body
259  
    // For HEAD requests, don't send body
260  
    if(rp.req.method() == method::head)
260  
    if(rp.req.method() == method::head)
261  
    {
261  
    {
262  
        auto [ec] = co_await rp.send("");
262  
        auto [ec] = co_await rp.send("");
263  
        if(ec)
263  
        if(ec)
264  
            co_return route_error(ec);
264  
            co_return route_error(ec);
265  
        co_return route_done;
265  
        co_return route_done;
266  
    }
266  
    }
267  

267  

268  
    // Open and stream the file
268  
    // Open and stream the file
269  
    file f;
269  
    file f;
270  
    system::error_code ec;
270  
    system::error_code ec;
271  
    f.open(path.c_str(), file_mode::scan, ec);
271  
    f.open(path.c_str(), file_mode::scan, ec);
272  
    if(ec)
272  
    if(ec)
273  
    {
273  
    {
274  
        if(impl_->opts.fallthrough)
274  
        if(impl_->opts.fallthrough)
275  
            co_return route_next;
275  
            co_return route_next;
276  
        rp.res.set_status(status::internal_server_error);
276  
        rp.res.set_status(status::internal_server_error);
277  
        auto [ec2] = co_await rp.send("Internal Server Error");
277  
        auto [ec2] = co_await rp.send("Internal Server Error");
278  
        if(ec2)
278  
        if(ec2)
279  
            co_return route_error(ec2);
279  
            co_return route_error(ec2);
280  
        co_return route_done;
280  
        co_return route_done;
281  
    }
281  
    }
282  

282  

283  
    // Seek to range start if needed
283  
    // Seek to range start if needed
284  
    if(info.is_range && info.range_start > 0)
284  
    if(info.is_range && info.range_start > 0)
285  
    {
285  
    {
286  
        f.seek(static_cast<std::uint64_t>(info.range_start), ec);
286  
        f.seek(static_cast<std::uint64_t>(info.range_start), ec);
287  
        if(ec.failed())
287  
        if(ec.failed())
288  
        {
288  
        {
289  
            rp.res.set_status(status::internal_server_error);
289  
            rp.res.set_status(status::internal_server_error);
290  
            auto [ec2] = co_await rp.send("Internal Server Error");
290  
            auto [ec2] = co_await rp.send("Internal Server Error");
291  
            if(ec2)
291  
            if(ec2)
292  
                co_return route_error(ec2);
292  
                co_return route_error(ec2);
293  
            co_return route_done;
293  
            co_return route_done;
294  
        }
294  
        }
295  
    }
295  
    }
296  

296  

297  
    // Calculate how much to send
297  
    // Calculate how much to send
298  
    std::int64_t remaining = info.range_end - info.range_start + 1;
298  
    std::int64_t remaining = info.range_end - info.range_start + 1;
299  

299  

300  
    // Stream file content
300  
    // Stream file content
301  
    constexpr std::size_t buf_size = 16384;
301  
    constexpr std::size_t buf_size = 16384;
302  
    char buffer[buf_size];
302  
    char buffer[buf_size];
303  

303  

304  
    while(remaining > 0)
304  
    while(remaining > 0)
305  
    {
305  
    {
306  
        auto const to_read = static_cast<std::size_t>(
306  
        auto const to_read = static_cast<std::size_t>(
307  
            (std::min)(remaining, static_cast<std::int64_t>(buf_size)));
307  
            (std::min)(remaining, static_cast<std::int64_t>(buf_size)));
308  

308  

309  
        auto const n1 = f.read(buffer, to_read, ec);
309  
        auto const n1 = f.read(buffer, to_read, ec);
310  
        if(ec.failed() || n1 == 0)
310  
        if(ec.failed() || n1 == 0)
311  
            break;
311  
            break;
312  

312  

313  
        auto [ec2, n2] = co_await rp.res_body.write(
313  
        auto [ec2, n2] = co_await rp.res_body.write(
314  
            capy::const_buffer(buffer, n1));
314  
            capy::const_buffer(buffer, n1));
315  
        (void)n2;
315  
        (void)n2;
316  
        if(ec2)
316  
        if(ec2)
317  
            co_return route_error(ec2);
317  
            co_return route_error(ec2);
318  
        remaining -= static_cast<std::int64_t>(n1);
318  
        remaining -= static_cast<std::int64_t>(n1);
319  
    }
319  
    }
320  

320  

321  
    auto [ec3] = co_await rp.res_body.write_eof();
321  
    auto [ec3] = co_await rp.res_body.write_eof();
322  
    if(ec3)
322  
    if(ec3)
323  
        co_return route_error(ec3);
323  
        co_return route_error(ec3);
324  
    co_return route_done;
324  
    co_return route_done;
325  
}
325  
}
326  

326  

327  
} // http
327  
} // http
328  
} // boost
328  
} // boost