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/fresh.hpp>
10  
#include <boost/http/server/fresh.hpp>
11  
#include <boost/http/field.hpp>
11  
#include <boost/http/field.hpp>
12  

12  

13  
namespace boost {
13  
namespace boost {
14  
namespace http {
14  
namespace http {
15  

15  

16  
namespace {
16  
namespace {
17  

17  

18  
// Check if ETag matches If-None-Match
18  
// Check if ETag matches If-None-Match
19  
// Returns true if they match (response is fresh)
19  
// Returns true if they match (response is fresh)
20  
bool
20  
bool
21  
etag_matches(
21  
etag_matches(
22  
    core::string_view if_none_match,
22  
    core::string_view if_none_match,
23  
    core::string_view etag ) noexcept
23  
    core::string_view etag ) noexcept
24  
{
24  
{
25  
    if( if_none_match.empty() || etag.empty() )
25  
    if( if_none_match.empty() || etag.empty() )
26  
        return false;
26  
        return false;
27  

27  

28  
    // "*" matches any ETag
28  
    // "*" matches any ETag
29  
    if( if_none_match == "*" )
29  
    if( if_none_match == "*" )
30  
        return true;
30  
        return true;
31  

31  

32  
    // Simple comparison - check if ETag appears in the list
32  
    // Simple comparison - check if ETag appears in the list
33  
    // In full implementation, would need to handle weak vs strong
33  
    // In full implementation, would need to handle weak vs strong
34  
    // and parse comma-separated list properly
34  
    // and parse comma-separated list properly
35  

35  

36  
    // Remove W/ prefix for comparison if present
36  
    // Remove W/ prefix for comparison if present
37  
    auto strip_weak = []( core::string_view s ) -> core::string_view
37  
    auto strip_weak = []( core::string_view s ) -> core::string_view
38  
    {
38  
    {
39  
        if( s.size() >= 2 &&
39  
        if( s.size() >= 2 &&
40  
            ( s[0] == 'W' || s[0] == 'w' ) &&
40  
            ( s[0] == 'W' || s[0] == 'w' ) &&
41  
            s[1] == '/' )
41  
            s[1] == '/' )
42  
            return s.substr( 2 );
42  
            return s.substr( 2 );
43  
        return s;
43  
        return s;
44  
    };
44  
    };
45  

45  

46  
    auto const etag_val = strip_weak( etag );
46  
    auto const etag_val = strip_weak( etag );
47  

47  

48  
    // Simple contains check for the ETag value
48  
    // Simple contains check for the ETag value
49  
    auto pos = if_none_match.find( etag_val );
49  
    auto pos = if_none_match.find( etag_val );
50  
    if( pos != core::string_view::npos )
50  
    if( pos != core::string_view::npos )
51  
        return true;
51  
        return true;
52  

52  

53  
    // Also check without weak prefix in if_none_match
53  
    // Also check without weak prefix in if_none_match
54  
    auto const inm_stripped = strip_weak( if_none_match );
54  
    auto const inm_stripped = strip_weak( if_none_match );
55  
    if( inm_stripped == etag_val )
55  
    if( inm_stripped == etag_val )
56  
        return true;
56  
        return true;
57  

57  

58  
    return false;
58  
    return false;
59  
}
59  
}
60  

60  

61  
// Parse HTTP date and compare
61  
// Parse HTTP date and compare
62  
// Returns true if response's Last-Modified <= request's If-Modified-Since
62  
// Returns true if response's Last-Modified <= request's If-Modified-Since
63  
// For simplicity, doing string comparison (works for RFC 7231 dates)
63  
// For simplicity, doing string comparison (works for RFC 7231 dates)
64  
bool
64  
bool
65  
not_modified_since(
65  
not_modified_since(
66  
    core::string_view if_modified_since,
66  
    core::string_view if_modified_since,
67  
    core::string_view last_modified ) noexcept
67  
    core::string_view last_modified ) noexcept
68  
{
68  
{
69  
    if( if_modified_since.empty() || last_modified.empty() )
69  
    if( if_modified_since.empty() || last_modified.empty() )
70  
        return false;
70  
        return false;
71  

71  

72  
    // HTTP dates in RFC 7231 format are lexicographically comparable
72  
    // HTTP dates in RFC 7231 format are lexicographically comparable
73  
    // when in the same format (preferred format)
73  
    // when in the same format (preferred format)
74  
    // For a robust implementation, would parse dates properly
74  
    // For a robust implementation, would parse dates properly
75  
    return last_modified <= if_modified_since;
75  
    return last_modified <= if_modified_since;
76  
}
76  
}
77  

77  

78  
} // (anon)
78  
} // (anon)
79  

79  

80  
bool
80  
bool
81  
is_fresh(
81  
is_fresh(
82  
    request const& req,
82  
    request const& req,
83  
    response const& res ) noexcept
83  
    response const& res ) noexcept
84  
{
84  
{
85  
    // Get conditional request headers
85  
    // Get conditional request headers
86  
    auto const if_none_match = req.value_or(
86  
    auto const if_none_match = req.value_or(
87  
        field::if_none_match, "" );
87  
        field::if_none_match, "" );
88  
    auto const if_modified_since = req.value_or(
88  
    auto const if_modified_since = req.value_or(
89  
        field::if_modified_since, "" );
89  
        field::if_modified_since, "" );
90  

90  

91  
    // If no conditional headers, not fresh
91  
    // If no conditional headers, not fresh
92  
    if( if_none_match.empty() && if_modified_since.empty() )
92  
    if( if_none_match.empty() && if_modified_since.empty() )
93  
        return false;
93  
        return false;
94  

94  

95  
    // Get response caching headers
95  
    // Get response caching headers
96  
    auto const etag = res.value_or( field::etag, "" );
96  
    auto const etag = res.value_or( field::etag, "" );
97  
    auto const last_modified = res.value_or(
97  
    auto const last_modified = res.value_or(
98  
        field::last_modified, "" );
98  
        field::last_modified, "" );
99  

99  

100  
    // Check ETag first (stronger validator)
100  
    // Check ETag first (stronger validator)
101  
    if( ! if_none_match.empty() )
101  
    if( ! if_none_match.empty() )
102  
    {
102  
    {
103  
        if( ! etag.empty() && etag_matches( if_none_match, etag ) )
103  
        if( ! etag.empty() && etag_matches( if_none_match, etag ) )
104  
            return true;
104  
            return true;
105  
        // If If-None-Match present but doesn't match, not fresh
105  
        // If If-None-Match present but doesn't match, not fresh
106  
        return false;
106  
        return false;
107  
    }
107  
    }
108  

108  

109  
    // Fall back to If-Modified-Since
109  
    // Fall back to If-Modified-Since
110  
    if( ! if_modified_since.empty() && ! last_modified.empty() )
110  
    if( ! if_modified_since.empty() && ! last_modified.empty() )
111  
    {
111  
    {
112  
        return not_modified_since( if_modified_since, last_modified );
112  
        return not_modified_since( if_modified_since, last_modified );
113  
    }
113  
    }
114  

114  

115  
    return false;
115  
    return false;
116  
}
116  
}
117  

117  

118  
} // http
118  
} // http
119  
} // boost
119  
} // boost