/src/mod_auth_openidc/src/cache/file.c
Line | Count | Source |
1 | | /* |
2 | | * Licensed to the Apache Software Foundation (ASF) under one |
3 | | * or more contributor license agreements. See the NOTICE file |
4 | | * distributed with this work for additional information |
5 | | * regarding copyright ownership. The ASF licenses this file |
6 | | * to you under the Apache License, Version 2.0 (the |
7 | | * "License"); you may not use this file except in compliance |
8 | | * with the License. You may obtain a copy of the License at |
9 | | * |
10 | | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | | * |
12 | | * Unless required by applicable law or agreed to in writing, |
13 | | * software distributed under the License is distributed on an |
14 | | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
15 | | * KIND, either express or implied. See the License for the |
16 | | * specific language governing permissions and limitations |
17 | | * under the License. |
18 | | */ |
19 | | |
20 | | /*************************************************************************** |
21 | | * Copyright (C) 2017-2026 ZmartZone Holding BV |
22 | | * Copyright (C) 2013-2017 Ping Identity Corporation |
23 | | * All rights reserved. |
24 | | * |
25 | | * DISCLAIMER OF WARRANTIES: |
26 | | * |
27 | | * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT |
28 | | * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, |
29 | | * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, |
30 | | * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY |
31 | | * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE |
32 | | * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET |
33 | | * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE |
34 | | * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR |
35 | | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
36 | | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF |
37 | | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
38 | | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
39 | | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
40 | | * |
41 | | * caching using a file storage backend |
42 | | * |
43 | | * @Author: Hans Zandbelt - hans.zandbelt@openidc.com |
44 | | */ |
45 | | |
46 | | #include "cfg/cache.h" |
47 | | #include "cfg/cfg_int.h" |
48 | | #include "util/util.h" |
49 | | |
50 | | /* |
51 | | * header structure that holds the metadata info for a cache file entry |
52 | | */ |
53 | | typedef struct { |
54 | | /* length of the cached data */ |
55 | | apr_size_t len; |
56 | | /* cache expiry timestamp */ |
57 | | apr_time_t expire; |
58 | | } oidc_cache_file_info_t; |
59 | | |
60 | | /* |
61 | | * prefix that distinguishes mod_auth_openidc cache files from other files in the same directory (/tmp) |
62 | | */ |
63 | 0 | #define OIDC_CACHE_FILE_PREFIX "mod-auth-openidc-" |
64 | | |
65 | | /* post config routine */ |
66 | 0 | int oidc_cache_file_post_config(apr_pool_t *pool, server_rec *s) { |
67 | 0 | apr_status_t rv = APR_SUCCESS; |
68 | 0 | oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(s->module_config, &auth_openidc_module); |
69 | 0 | if (cfg->cache.file_dir == NULL) { |
70 | | /* by default we'll use the OS specified /tmp dir for cache files */ |
71 | 0 | rv = apr_temp_dir_get((const char **)&cfg->cache.file_dir, pool); |
72 | 0 | if (rv != APR_SUCCESS) { |
73 | 0 | oidc_serror(s, "apr_temp_dir_get failed: could not find a temp dir: %s", |
74 | 0 | oidc_cache_status2str(pool, rv)); |
75 | 0 | return HTTP_INTERNAL_SERVER_ERROR; |
76 | 0 | } |
77 | 0 | } |
78 | 0 | return OK; |
79 | 0 | } |
80 | | |
81 | | /* |
82 | | * return the cache file name for a specified key |
83 | | */ |
84 | 0 | static const char *oidc_cache_file_name(request_rec *r, const char *section, const char *key) { |
85 | 0 | return apr_psprintf(r->pool, "%s%s-%s", OIDC_CACHE_FILE_PREFIX, section, oidc_http_url_encode(r, key)); |
86 | 0 | } |
87 | | |
88 | | /* |
89 | | * return the fully qualified path name to a cache file for a specified key |
90 | | */ |
91 | 0 | static const char *oidc_cache_file_path(request_rec *r, const char *section, const char *key) { |
92 | 0 | oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); |
93 | 0 | return apr_psprintf(r->pool, "%s/%s", cfg->cache.file_dir, oidc_cache_file_name(r, section, key)); |
94 | 0 | } |
95 | | |
96 | | /* |
97 | | * read a specified number of bytes from a cache file in to a preallocated buffer |
98 | | */ |
99 | | static apr_status_t oidc_cache_file_read(request_rec *r, const char *path, apr_file_t *fd, void *buf, |
100 | 0 | const apr_size_t len) { |
101 | |
|
102 | 0 | apr_status_t rc = APR_SUCCESS; |
103 | 0 | apr_size_t bytes_read = 0; |
104 | 0 | char s_err[128]; |
105 | | |
106 | | /* (blocking) read the requested number of bytes */ |
107 | 0 | rc = apr_file_read_full(fd, buf, len, &bytes_read); |
108 | | |
109 | | /* test for system errors */ |
110 | 0 | if (rc != APR_SUCCESS) { |
111 | 0 | oidc_error(r, "could not read from: %s (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); |
112 | 0 | } |
113 | | |
114 | | /* ensure that we've got the requested number of bytes */ |
115 | 0 | if (bytes_read != len) { |
116 | 0 | oidc_error(r, |
117 | 0 | "could not read enough bytes from: \"%s\", bytes_read (%" APR_SIZE_T_FMT |
118 | 0 | ") != len (%" APR_SIZE_T_FMT ")", |
119 | 0 | path, bytes_read, len); |
120 | 0 | rc = APR_EGENERAL; |
121 | 0 | } |
122 | |
|
123 | 0 | return rc; |
124 | 0 | } |
125 | | |
126 | | /* |
127 | | * write a specified number of bytes from a buffer to a cache file |
128 | | */ |
129 | | static apr_status_t oidc_cache_file_write(request_rec *r, const char *path, apr_file_t *fd, const void *buf, |
130 | 0 | const apr_size_t len) { |
131 | |
|
132 | 0 | apr_status_t rc = APR_SUCCESS; |
133 | 0 | apr_size_t bytes_written = 0; |
134 | 0 | char s_err[128]; |
135 | | |
136 | | /* (blocking) write the number of bytes in the buffer */ |
137 | 0 | rc = apr_file_write_full(fd, buf, len, &bytes_written); |
138 | | |
139 | | /* check for a system error */ |
140 | 0 | if (rc != APR_SUCCESS) { |
141 | 0 | oidc_error(r, "could not write to: \"%s\" (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); |
142 | 0 | return rc; |
143 | 0 | } |
144 | | |
145 | | /* check that all bytes from the header were written */ |
146 | 0 | if (bytes_written != len) { |
147 | 0 | oidc_error(r, |
148 | 0 | "could not write enough bytes to: \"%s\", bytes_written (%" APR_SIZE_T_FMT |
149 | 0 | ") != len (%" APR_SIZE_T_FMT ")", |
150 | 0 | path, bytes_written, len); |
151 | 0 | return APR_EGENERAL; |
152 | 0 | } |
153 | | |
154 | 0 | return rc; |
155 | 0 | } |
156 | | |
157 | | /* |
158 | | * get a value for the specified key from the cache |
159 | | */ |
160 | 0 | static apr_byte_t oidc_cache_file_get(request_rec *r, const char *section, const char *key, char **value) { |
161 | 0 | apr_file_t *fd = NULL; |
162 | 0 | apr_status_t rc = APR_SUCCESS; |
163 | 0 | apr_finfo_t finfo; |
164 | 0 | char s_err[128]; |
165 | | |
166 | | /* get the fully qualified path to the cache file based on the key name */ |
167 | 0 | const char *path = oidc_cache_file_path(r, section, key); |
168 | | |
169 | | /* reject links and non-regular files before opening the cache entry (no following here, |
170 | | * unlike the metadata/template reads: this module writes this directory itself and never |
171 | | * creates symlinks in it, so one can only have been planted) */ |
172 | 0 | if (oidc_util_file_is_regular(r->pool, path, FALSE, &finfo) == FALSE) { |
173 | 0 | if (finfo.filetype == APR_NOFILE) { |
174 | 0 | oidc_debug(r, "cache miss for key \"%s\"", key); |
175 | 0 | } else { |
176 | 0 | oidc_warn(r, "ignoring non-regular cache file \"%s\"", path); |
177 | 0 | } |
178 | 0 | return TRUE; |
179 | 0 | } |
180 | | |
181 | | /* open the cache file if it exists, otherwise we just have a "regular" cache miss */ |
182 | 0 | if (apr_file_open(&fd, path, APR_FOPEN_READ | APR_FOPEN_BUFFERED, APR_OS_DEFAULT, r->pool) != APR_SUCCESS) { |
183 | 0 | oidc_debug(r, "cache miss for key \"%s\"", key); |
184 | 0 | return TRUE; |
185 | 0 | } |
186 | | |
187 | | /* Shared locks allow concurrent reads while still excluding the expiry cleaner. */ |
188 | 0 | apr_file_lock(fd, APR_FLOCK_SHARED); |
189 | | |
190 | | /* move the read pointer to the very start of the cache file */ |
191 | 0 | apr_off_t begin = 0; |
192 | 0 | apr_file_seek(fd, APR_SET, &begin); |
193 | 0 | if ((rc = apr_file_info_get(&finfo, APR_FINFO_SIZE, fd)) != APR_SUCCESS) |
194 | 0 | goto error_close; |
195 | 0 | if ((finfo.size < (apr_off_t)sizeof(oidc_cache_file_info_t)) || |
196 | 0 | (finfo.size - (apr_off_t)sizeof(oidc_cache_file_info_t) > (apr_off_t)OIDC_UTIL_FILE_SIZE_MAX)) { |
197 | 0 | oidc_error(r, "cache file \"%s\" has an invalid size (%" APR_OFF_T_FMT ")", path, finfo.size); |
198 | 0 | rc = APR_EGENERAL; |
199 | 0 | goto error_close; |
200 | 0 | } |
201 | | |
202 | | /* read a header with metadata */ |
203 | 0 | oidc_cache_file_info_t info; |
204 | 0 | if ((rc = oidc_cache_file_read(r, path, fd, &info, sizeof(oidc_cache_file_info_t))) != APR_SUCCESS) |
205 | 0 | goto error_close; |
206 | | |
207 | | /* check if this cache entry has already expired */ |
208 | 0 | if (apr_time_now() >= info.expire) { |
209 | | |
210 | | /* |
211 | | * Leave expired files for the cleaner. Unlinking here could race a writer's atomic rename |
212 | | * and remove the replacement entry. |
213 | | */ |
214 | 0 | apr_file_unlock(fd); |
215 | 0 | apr_file_close(fd); |
216 | |
|
217 | 0 | oidc_debug(r, "cache entry \"%s\" expired, leaving file \"%s\" to the cleaning cycle", key, path); |
218 | | |
219 | | /* nothing strange happened really */ |
220 | 0 | return TRUE; |
221 | 0 | } |
222 | 0 | if ((info.len == 0) || (info.len > OIDC_UTIL_FILE_SIZE_MAX) || |
223 | 0 | ((apr_off_t)info.len > finfo.size - (apr_off_t)sizeof(oidc_cache_file_info_t))) { |
224 | 0 | oidc_error(r, "cache file \"%s\" has an invalid value length (%" APR_SIZE_T_FMT ")", path, info.len); |
225 | 0 | rc = APR_EGENERAL; |
226 | 0 | goto error_close; |
227 | 0 | } |
228 | | |
229 | | /* allocate space for the actual value based on the data size info in the header (+1 for \0 termination) */ |
230 | 0 | *value = apr_palloc(r->pool, info.len + 1); |
231 | | |
232 | | /* (blocking) read the requested data in to the buffer */ |
233 | 0 | rc = oidc_cache_file_read(r, path, fd, (void *)(*value), info.len); |
234 | | |
235 | | /* barf on failure */ |
236 | 0 | if (rc != APR_SUCCESS) { |
237 | 0 | oidc_error(r, "could not read cache value from \"%s\"", path); |
238 | 0 | goto error_close; |
239 | 0 | } |
240 | | |
241 | | /* NUL-terminate: an entry this module wrote already ends in \0 within info.len, but a corrupt or |
242 | | * externally-planted cache file may not, and every caller treats the value as a C string */ |
243 | 0 | (*value)[info.len] = '\0'; |
244 | | |
245 | | /* we're done, unlock and close the file */ |
246 | 0 | apr_file_unlock(fd); |
247 | 0 | apr_file_close(fd); |
248 | |
|
249 | 0 | return TRUE; |
250 | | |
251 | 0 | error_close: |
252 | |
|
253 | 0 | apr_file_unlock(fd); |
254 | 0 | apr_file_close(fd); |
255 | |
|
256 | 0 | oidc_error(r, "return error status %d (%s)", rc, apr_strerror(rc, s_err, sizeof(s_err))); |
257 | |
|
258 | 0 | return FALSE; |
259 | 0 | } |
260 | | |
261 | | // NB: the file-cache housekeeping parameters are compile-time constants (not configurable) |
262 | 0 | #define OIDC_CACHE_FILE_LAST_CLEANED "last-cleaned" |
263 | | |
264 | | /* |
265 | | * decide whether a cache cleaning cycle should run and bump the "last cleaned" |
266 | | * timestamp; creates the metadata file if it does not yet exist |
267 | | * returns TRUE if cleaning should proceed; *rc holds the status to return when FALSE |
268 | | */ |
269 | | static apr_byte_t oidc_cache_file_clean_due(request_rec *r, const oidc_cfg_t *cfg, const char *metadata_path, |
270 | 0 | apr_status_t *rc) { |
271 | 0 | apr_file_t *fd = NULL; |
272 | 0 | apr_finfo_t fi; |
273 | 0 | char s_err[128]; |
274 | |
|
275 | 0 | *rc = APR_SUCCESS; |
276 | | |
277 | | /* no metadata file yet: create it and proceed with a first cleaning cycle */ |
278 | 0 | if (apr_stat(&fi, metadata_path, APR_FINFO_MTIME, r->pool) != APR_SUCCESS) { |
279 | 0 | if ((*rc = apr_file_open(&fd, metadata_path, (APR_FOPEN_WRITE | APR_FOPEN_CREATE), |
280 | 0 | (APR_FPROT_UREAD | APR_FPROT_UWRITE), r->pool)) != APR_SUCCESS) { |
281 | 0 | oidc_error(r, "error creating cache timestamp file '%s' (%s)", metadata_path, |
282 | 0 | apr_strerror(*rc, s_err, sizeof(s_err))); |
283 | 0 | return FALSE; |
284 | 0 | } |
285 | 0 | if ((*rc = apr_file_close(fd)) != APR_SUCCESS) { |
286 | 0 | oidc_error(r, "error closing cache timestamp file '%s' (%s)", metadata_path, |
287 | 0 | apr_strerror(*rc, s_err, sizeof(s_err))); |
288 | 0 | } |
289 | 0 | *rc = APR_SUCCESS; |
290 | 0 | return TRUE; |
291 | 0 | } |
292 | | |
293 | | /* really only clean once per so much time, check that we have not recently run */ |
294 | 0 | if (apr_time_now() < fi.mtime + apr_time_from_sec(oidc_cfg_cache_file_clean_interval_get(cfg))) { |
295 | 0 | oidc_debug(r, |
296 | 0 | "last cleanup call was less than %d seconds ago (next one as early as in %" APR_TIME_T_FMT |
297 | 0 | " secs)", |
298 | 0 | oidc_cfg_cache_file_clean_interval_get(cfg), |
299 | 0 | apr_time_sec(fi.mtime + apr_time_from_sec(oidc_cfg_cache_file_clean_interval_get(cfg)) - |
300 | 0 | apr_time_now())); |
301 | 0 | return FALSE; |
302 | 0 | } |
303 | | |
304 | | /* time to clean, reset the mtime of the metadata file to reflect this cleaning cycle */ |
305 | 0 | apr_file_mtime_set(metadata_path, apr_time_now(), r->pool); |
306 | |
|
307 | 0 | oidc_debug(r, "start cleaning cycle"); |
308 | |
|
309 | 0 | return TRUE; |
310 | 0 | } |
311 | | |
312 | | /* |
313 | | * Recheck the device/inode before unlinking so a concurrent atomic replacement is not removed. |
314 | | * Platforms without that identity information retain the unconditional cleanup behavior. |
315 | | */ |
316 | | static apr_byte_t oidc_cache_file_is_same(request_rec *r, const char *path, const apr_finfo_t *ident, |
317 | 0 | apr_byte_t have_ident) { |
318 | 0 | apr_finfo_t current; |
319 | |
|
320 | 0 | if (have_ident == FALSE) |
321 | 0 | return TRUE; |
322 | | |
323 | | /* gone already (another cleaner, or a delete): nothing of ours left to remove */ |
324 | 0 | if (apr_stat(¤t, path, APR_FINFO_IDENT, r->pool) != APR_SUCCESS) |
325 | 0 | return FALSE; |
326 | | |
327 | 0 | if ((current.valid & APR_FINFO_IDENT) != APR_FINFO_IDENT) |
328 | 0 | return TRUE; |
329 | | |
330 | 0 | return ((current.device == ident->device) && (current.inode == ident->inode)) ? TRUE : FALSE; |
331 | 0 | } |
332 | | |
333 | | /* |
334 | | * process a single cache directory entry: remove the file if it expired or its header is corrupt |
335 | | */ |
336 | | static void oidc_cache_file_clean_entry(request_rec *r, oidc_cfg_t *cfg, const char *metadata_filename, |
337 | 0 | const apr_finfo_t *fi) { |
338 | 0 | apr_file_t *fd = NULL; |
339 | 0 | apr_status_t rc; |
340 | 0 | oidc_cache_file_info_t info; |
341 | 0 | apr_finfo_t ident; |
342 | 0 | apr_byte_t have_ident = FALSE; |
343 | 0 | char s_err[128]; |
344 | | |
345 | | /* skip non-cache entries, cq. the ".", ".." and the metadata file */ |
346 | 0 | if ((fi->name[0] == OIDC_CHAR_DOT) || (_oidc_strstr(fi->name, OIDC_CACHE_FILE_PREFIX) != fi->name) || |
347 | 0 | (_oidc_strcmp(fi->name, metadata_filename) == 0)) |
348 | 0 | return; |
349 | | |
350 | | /* get the fully qualified path to the cache file and open it */ |
351 | 0 | const char *path = apr_psprintf(r->pool, "%s/%s", cfg->cache.file_dir, fi->name); |
352 | 0 | if ((rc = apr_file_open(&fd, path, APR_FOPEN_READ, APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) { |
353 | 0 | oidc_error(r, "unable to open cache entry \"%s\" (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); |
354 | 0 | return; |
355 | 0 | } |
356 | | |
357 | | /* note which file this actually is, so that only this one can be removed below */ |
358 | 0 | have_ident = ((apr_file_info_get(&ident, APR_FINFO_IDENT, fd) == APR_SUCCESS) && |
359 | 0 | ((ident.valid & APR_FINFO_IDENT) == APR_FINFO_IDENT)) |
360 | 0 | ? TRUE |
361 | 0 | : FALSE; |
362 | | |
363 | | /* read the header with cache metadata info */ |
364 | 0 | apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); |
365 | 0 | rc = oidc_cache_file_read(r, path, fd, &info, sizeof(oidc_cache_file_info_t)); |
366 | 0 | apr_file_unlock(fd); |
367 | | /* close before removing: Windows will not unlink a file this process still holds open */ |
368 | 0 | apr_file_close(fd); |
369 | |
|
370 | 0 | if (rc != APR_SUCCESS) { |
371 | 0 | oidc_error(r, "cache entry (%s) corrupted (%s), removing file \"%s\"", fi->name, |
372 | 0 | apr_strerror(rc, s_err, sizeof(s_err)), path); |
373 | 0 | } else if (apr_time_now() < info.expire) { |
374 | | /* entry is still valid, keep it */ |
375 | 0 | return; |
376 | 0 | } else { |
377 | 0 | oidc_debug(r, "cache entry (%s) expired, removing file \"%s\")", fi->name, path); |
378 | 0 | } |
379 | | |
380 | | /* a writer may have replaced the entry since it was read; only remove the file inspected above */ |
381 | 0 | if (oidc_cache_file_is_same(r, path, &ident, have_ident) == FALSE) { |
382 | 0 | oidc_debug(r, "cache entry (%s) is no longer the file that was inspected, leaving it alone", fi->name); |
383 | 0 | return; |
384 | 0 | } |
385 | | |
386 | | /* delete the cache file */ |
387 | 0 | if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) { |
388 | | /* hrm, this will most probably happen again on the next run... */ |
389 | 0 | oidc_error(r, "could not delete cache file \"%s\" (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); |
390 | 0 | } |
391 | 0 | } |
392 | | |
393 | | /* |
394 | | * delete all expired entries from the cache directory |
395 | | */ |
396 | 0 | static apr_status_t oidc_cache_file_clean(request_rec *r) { |
397 | 0 | apr_status_t rc = APR_SUCCESS; |
398 | 0 | apr_dir_t *dir = NULL; |
399 | 0 | apr_status_t i; |
400 | 0 | apr_finfo_t fi; |
401 | 0 | char s_err[128]; |
402 | |
|
403 | 0 | oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); |
404 | | |
405 | | /* get the path to the metadata file that holds "last cleaned" metadata info */ |
406 | 0 | const char *metadata_path = oidc_cache_file_path(r, "cache-file", OIDC_CACHE_FILE_LAST_CLEANED); |
407 | | |
408 | | /* decide whether we should run a cleaning cycle (and bump the timestamp if so) */ |
409 | 0 | if (oidc_cache_file_clean_due(r, cfg, metadata_path, &rc) == FALSE) |
410 | 0 | return rc; |
411 | | |
412 | | /* time to clean, open the cache directory */ |
413 | 0 | if ((rc = apr_dir_open(&dir, cfg->cache.file_dir, r->pool)) != APR_SUCCESS) { |
414 | 0 | oidc_error(r, "error opening cache directory '%s' for cleaning (%s)", cfg->cache.file_dir, |
415 | 0 | apr_strerror(rc, s_err, sizeof(s_err))); |
416 | 0 | return rc; |
417 | 0 | } |
418 | | |
419 | 0 | const char *metadata_filename = oidc_cache_file_name(r, "cache-file", OIDC_CACHE_FILE_LAST_CLEANED); |
420 | | |
421 | | /* loop trough the cache file entries */ |
422 | 0 | do { |
423 | 0 | i = apr_dir_read(&fi, APR_FINFO_NAME, dir); |
424 | 0 | if (i == APR_SUCCESS) |
425 | 0 | oidc_cache_file_clean_entry(r, cfg, metadata_filename, &fi); |
426 | 0 | } while (i == APR_SUCCESS); |
427 | |
|
428 | 0 | apr_dir_close(dir); |
429 | |
|
430 | 0 | return APR_SUCCESS; |
431 | 0 | } |
432 | | |
433 | | /* |
434 | | * write a value for the specified key to the cache |
435 | | */ |
436 | | static apr_byte_t oidc_cache_file_set(request_rec *r, const char *section, const char *key, const char *value, |
437 | 0 | apr_time_t expiry) { |
438 | 0 | apr_file_t *fd = NULL; |
439 | 0 | apr_status_t rc = APR_SUCCESS; |
440 | 0 | char s_err[128]; |
441 | 0 | char *rnd = NULL; |
442 | |
|
443 | 0 | if (oidc_util_rand_str(r, &rnd, 12) == FALSE) |
444 | 0 | return FALSE; |
445 | | |
446 | | /* get the fully qualified path to the cache file based on the key name */ |
447 | 0 | const char *target = oidc_cache_file_path(r, section, key); |
448 | 0 | const char *path = apr_psprintf(r->pool, "%s.%s.tmp", target, rnd); |
449 | | |
450 | | /* only on writes (not on reads) we clean the cache first (if not done recently) */ |
451 | 0 | oidc_cache_file_clean(r); |
452 | | |
453 | | /* just remove cache file if value is NULL */ |
454 | 0 | if (value == NULL) { |
455 | 0 | if ((rc = apr_file_remove(target, r->pool)) != APR_SUCCESS) { |
456 | 0 | oidc_error(r, "could not delete cache file \"%s\" (%s)", path, |
457 | 0 | apr_strerror(rc, s_err, sizeof(s_err))); |
458 | 0 | } |
459 | 0 | return TRUE; |
460 | 0 | } |
461 | | |
462 | | /* try to open the cache file for writing, creating it if it does not exist; cache entries can |
463 | | * hold session/state secrets, so restrict permissions to the owner only */ |
464 | 0 | if ((rc = apr_file_open(&fd, path, (APR_FOPEN_WRITE | APR_FOPEN_CREATE), (APR_FPROT_UREAD | APR_FPROT_UWRITE), |
465 | 0 | r->pool)) != APR_SUCCESS) { |
466 | 0 | oidc_error(r, "cache file \"%s\" could not be opened (%s)", path, |
467 | 0 | apr_strerror(rc, s_err, sizeof(s_err))); |
468 | 0 | return FALSE; |
469 | 0 | } |
470 | | |
471 | | /* lock the file and move the write pointer to the start of it */ |
472 | 0 | apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); |
473 | 0 | apr_off_t begin = 0; |
474 | 0 | apr_file_trunc(fd, begin); |
475 | 0 | apr_file_seek(fd, APR_SET, &begin); |
476 | | |
477 | | /* construct the metadata for this cache entry in the header info */ |
478 | 0 | oidc_cache_file_info_t info; |
479 | 0 | info.expire = expiry; |
480 | 0 | info.len = _oidc_strlen(value) + 1; |
481 | | |
482 | | /* write the header, then the value; both returns must be checked, since a short |
483 | | * write (a full disk, a quota) would otherwise be reported as a stored entry and |
484 | | * only surface later as an unreadable one */ |
485 | 0 | if (oidc_cache_file_write(r, path, fd, &info, sizeof(oidc_cache_file_info_t)) != APR_SUCCESS) |
486 | 0 | goto error; |
487 | | |
488 | 0 | if (oidc_cache_file_write(r, path, fd, (const void *)value, info.len) != APR_SUCCESS) |
489 | 0 | goto error; |
490 | | |
491 | | /* unlock and close the written file */ |
492 | 0 | apr_file_unlock(fd); |
493 | 0 | apr_file_close(fd); |
494 | |
|
495 | 0 | if ((rc = apr_file_rename(path, target, r->pool)) != APR_SUCCESS) { |
496 | 0 | oidc_error(r, "cache file: %s could not be renamed to: %s (%s)", path, target, |
497 | 0 | apr_strerror(rc, s_err, sizeof(s_err))); |
498 | 0 | apr_file_remove(path, r->pool); |
499 | 0 | return FALSE; |
500 | 0 | } |
501 | | |
502 | 0 | oidc_debug(r, "successfully stored entry for key \"%s\" in file of %" APR_SIZE_T_FMT " bytes", key, info.len); |
503 | |
|
504 | 0 | return TRUE; |
505 | | |
506 | 0 | error: |
507 | | |
508 | | /* drop the partially written temporary file rather than leaving it behind */ |
509 | 0 | apr_file_unlock(fd); |
510 | 0 | apr_file_close(fd); |
511 | 0 | apr_file_remove(path, r->pool); |
512 | |
|
513 | 0 | return FALSE; |
514 | 0 | } |
515 | | |
516 | | // clang-format off |
517 | | |
518 | | oidc_cache_t oidc_cache_file = { |
519 | | "file", |
520 | | 1, |
521 | | oidc_cache_file_post_config, |
522 | | NULL, |
523 | | oidc_cache_file_get, |
524 | | oidc_cache_file_set, |
525 | | NULL |
526 | | }; |
527 | | |
528 | | // clang-format on |