1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
//! Which files under a root may be read into the content cache.
//!
//! Enumeration only: nothing here opens a file, reads bytes, populates a map or serves
//! anything — see `PLAN-cache.md` for the commits that do. This is the security half, alone
//! and testable, because it is the half a throughput benchmark cannot check.
use ;
use ;
use Read as _;
use ;
use Bytes;
/// Ceiling on entries the walk will enumerate.
///
/// The walk cannot loop — it never follows a symlink, so it descends a finite real tree — but
/// "finite" is not a bound (A2). A root with a million files should stop, loudly, rather than
/// spend startup discovering that. Chosen well above any plausible static site and far below
/// anything that would make construction feel hung.
const MAX_CACHE_ENTRIES: usize = 65_536;
/// Whether a directory entry found by the population walk may be read into the cache.
///
/// Only a **real regular file**. A directory is descended rather than cached, and everything
/// else is refused: symlinks, FIFOs, sockets, block and character devices.
///
/// # Why refusing symlinks removes a whole hazard
///
/// The obvious rule would be "cache any file whose opened descriptor resolves inside the
/// root", reusing the containment check the request path already makes. That is the check
/// `resolve::open_verified` performs, and it is the reason there is no check-to-open window
/// when serving.
///
/// Refusing symlinks outright is stronger and needs none of it. If the walk never follows a
/// symlink — not for directories, not for files — then **every path it reaches is inside the
/// root by construction**, because it got there by descending real directories from a root
/// that was already canonicalised. There is nothing to verify, so there is no second
/// implementation of containment to keep in step with the first. That mattered enough to
/// design around: two independently derived answers to "where does this path point" is
/// exactly what let `/admin%2Fconfig` reach a nested file in 0.31.
///
/// The cost is stated rather than hidden: a symlinked file inside the root is not cached. It
/// still serves — the request falls through to the disk path, which verifies it on the
/// opened descriptor as it always has — it is simply served at disk speed.
///
/// # Why a FIFO is not merely "unusual"
///
/// `File::open` on a FIFO blocks until a writer appears. On the request path that costs one
/// request. During an eager walk at construction it would hang **startup**, so refusing it
/// here is what keeps population bounded (A2).
///
/// `FileType` comes from `DirEntry::file_type`, which does not traverse symlinks — so
/// `is_file()` is already false for a symlink pointing at a regular file. That is the
/// property this rule depends on, and it is why the check is one call rather than a `match`.
pub
// Unix-gated: every refusal under test — symlink, FIFO, socket — is a unix file type, and
// the fixtures need `std::os::unix` and `mkfifo` to create them. `is_file()` excludes the
// Windows equivalents (reparse points, named pipes) by the same rule, but not provably from
// here.
/// Every file under `root` that may be cached, sorted.
///
/// Descends real directories only. Because [`is_cacheable`] refuses symlinks and this walk
/// refuses to descend anything that is not a real directory, **every path returned is inside
/// `root` by construction** — which is why neither this function nor its callers need a
/// containment check. See [`is_cacheable`] for why that matters more than it looks.
///
/// An unreadable directory is skipped rather than fatal: a permissions problem on one
/// subdirectory should cost that subtree its caching, not the server its startup. Those paths
/// fall through to the disk path, which has always verified them per request.
///
/// Stops at [`MAX_CACHE_ENTRIES`], returning what it found. A truncated enumeration is a
/// smaller cache, not a broken one — every path not returned simply falls through to disk.
pub
/// [`cacheable_entries`] with the ceiling supplied.
///
/// Split out so the bound is testable. Asserting it against the real ceiling would need
/// 65,537 fixture files, and a test that instead creates sixty-four and checks
/// `len() <= 65_536` passes whether the ceiling is enforced or not — which is what the first
/// version of that test did, until the mutation run said so.
/// One file held in memory.
///
/// Stores the `Metadata` rather than a derived ETag on purpose: the serving path builds its
/// ETag with `generate_etag(&metadata)`, so a cached response calls **the same function on the
/// same input** and cannot drift from an uncached one. Deriving it here would be a second
/// implementation of a value that must match exactly.
pub
/// Every cached file, keyed by its path relative to the served root.
///
/// The key is a `PathBuf` — byte-exact, not a `String`. A lossy conversion would map two
/// different filenames onto one key, and a request could then be served another file's bytes.
pub
/// Read every cacheable file under `root` into memory, up to `max_bytes`.
///
/// Truncates rather than failing. Enumeration is sorted, so the files cached are a
/// deterministic prefix: two servers on identical roots hold the same set. Stopping at the
/// first file that would exceed the budget — rather than skipping it and continuing with
/// smaller ones — keeps that prefix property, which packing would destroy.
///
/// An unreadable file is skipped, not fatal. It falls through to the disk path, which has
/// always verified and served it; a permissions problem on one asset should not cost a
/// deployment its startup.
///
/// # Why there is no re-verification between the walk and the read
///
/// A file enumerated as a regular file could in principle be replaced by a symlink before it
/// is read, and `fs::read` would follow it — caching content from outside the root under an
/// in-root key. That race is not closed here, and deliberately so: **it requires write access
/// to the served root, and a writer who has that can simply write the file directly.** The
/// walk's symlink refusal exists for the operator who accidentally leaves a link pointing out
/// of the root, which is a configuration mistake rather than an attack, and it holds. Adding
/// `O_NOFOLLOW` would buy nothing an attacker with write access has not already got, at the
/// cost of a platform dependency.
pub
/// The sibling suffixes the request path probes for, in the same order.
///
/// Duplicated from `server::SIDECAR_ENCODINGS` only in the extensions, not the negotiation:
/// this asks "does a sibling exist", never "which one should be served".
const SIDECAR_EXTENSIONS: = ;