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-2021 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@zmartzone.eu
44 */
45
46 #include <apr_hash.h>
47 #include <apr_time.h>
48 #include <apr_strings.h>
49 #include <apr_pools.h>
50
51 #include <httpd.h>
52 #include <http_log.h>
53
54 #include "../mod_auth_openidc.h"
55
56 extern module AP_MODULE_DECLARE_DATA auth_openidc_module;
57
58 /*
59 * header structure that holds the metadata info for a cache file entry
60 */
61 typedef struct {
62 /* length of the cached data */
63 apr_size_t len;
64 /* cache expiry timestamp */
65 apr_time_t expire;
66 } oidc_cache_file_info_t;
67
68 /*
69 * prefix that distinguishes mod_auth_openidc cache files from other files in the same directory (/tmp)
70 */
71 #define OIDC_CACHE_FILE_PREFIX "mod-auth-openidc-"
72
73 /* post config routine */
oidc_cache_file_post_config(server_rec * s)74 int oidc_cache_file_post_config(server_rec *s) {
75 oidc_cfg *cfg = (oidc_cfg *) ap_get_module_config(s->module_config,
76 &auth_openidc_module);
77 if (cfg->cache_file_dir == NULL) {
78 /* by default we'll use the OS specified /tmp dir for cache files */
79 apr_temp_dir_get((const char **) &cfg->cache_file_dir,
80 s->process->pool);
81 }
82 return OK;
83 }
84
85 /*
86 * return the cache file name for a specified key
87 */
oidc_cache_file_name(request_rec * r,const char * section,const char * key)88 static const char *oidc_cache_file_name(request_rec *r, const char *section,
89 const char *key) {
90 return apr_psprintf(r->pool, "%s%s-%s", OIDC_CACHE_FILE_PREFIX, section,
91 oidc_util_escape_string(r, key));
92 }
93
94 /*
95 * return the fully qualified path name to a cache file for a specified key
96 */
oidc_cache_file_path(request_rec * r,const char * section,const char * key)97 static const char *oidc_cache_file_path(request_rec *r, const char *section,
98 const char *key) {
99 oidc_cfg *cfg = ap_get_module_config(r->server->module_config,
100 &auth_openidc_module);
101 return apr_psprintf(r->pool, "%s/%s", cfg->cache_file_dir,
102 oidc_cache_file_name(r, section, key));
103 }
104
105 /*
106 * read a specified number of bytes from a cache file in to a preallocated buffer
107 */
oidc_cache_file_read(request_rec * r,const char * path,apr_file_t * fd,void * buf,const apr_size_t len)108 static apr_status_t oidc_cache_file_read(request_rec *r, const char *path,
109 apr_file_t *fd, void *buf, const apr_size_t len) {
110
111 apr_status_t rc = APR_SUCCESS;
112 apr_size_t bytes_read = 0;
113 char s_err[128];
114
115 /* (blocking) read the requested number of bytes */
116 rc = apr_file_read_full(fd, buf, len, &bytes_read);
117
118 /* test for system errors */
119 if (rc != APR_SUCCESS) {
120 oidc_error(r, "could not read from: %s (%s)", path,
121 apr_strerror(rc, s_err, sizeof(s_err)));
122 }
123
124 /* ensure that we've got the requested number of bytes */
125 if (bytes_read != len) {
126 oidc_error(r,
127 "could not read enough bytes from: \"%s\", bytes_read (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")",
128 path, bytes_read, len);
129 rc = APR_EGENERAL;
130 }
131
132 return rc;
133 }
134
135 /*
136 * write a specified number of bytes from a buffer to a cache file
137 */
oidc_cache_file_write(request_rec * r,const char * path,apr_file_t * fd,void * buf,const apr_size_t len)138 static apr_status_t oidc_cache_file_write(request_rec *r, const char *path,
139 apr_file_t *fd, void *buf, const apr_size_t len) {
140
141 apr_status_t rc = APR_SUCCESS;
142 apr_size_t bytes_written = 0;
143 char s_err[128];
144
145 /* (blocking) write the number of bytes in the buffer */
146 rc = apr_file_write_full(fd, buf, len, &bytes_written);
147
148 /* check for a system error */
149 if (rc != APR_SUCCESS) {
150 oidc_error(r, "could not write to: \"%s\" (%s)", path,
151 apr_strerror(rc, s_err, sizeof(s_err)));
152 return rc;
153 }
154
155 /* check that all bytes from the header were written */
156 if (bytes_written != len) {
157 oidc_error(r,
158 "could not write enough bytes to: \"%s\", bytes_written (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")",
159 path, bytes_written, len);
160 return APR_EGENERAL;
161 }
162
163 return rc;
164 }
165
166 /*
167 * get a value for the specified key from the cache
168 */
oidc_cache_file_get(request_rec * r,const char * section,const char * key,const char ** value)169 static apr_byte_t oidc_cache_file_get(request_rec *r, const char *section,
170 const char *key, const char **value) {
171 apr_file_t *fd = NULL;
172 apr_status_t rc = APR_SUCCESS;
173 char s_err[128];
174
175 /* get the fully qualified path to the cache file based on the key name */
176 const char *path = oidc_cache_file_path(r, section, key);
177
178 /* open the cache file if it exists, otherwise we just have a "regular" cache miss */
179 if (apr_file_open(&fd, path, APR_FOPEN_READ | APR_FOPEN_BUFFERED,
180 APR_OS_DEFAULT, r->pool) != APR_SUCCESS) {
181 oidc_debug(r, "cache miss for key \"%s\"", key);
182 return TRUE;
183 }
184
185 /* the file exists, now lock it */
186 apr_file_lock(fd, APR_FLOCK_EXCLUSIVE);
187
188 /* move the read pointer to the very start of the cache file */
189 apr_off_t begin = 0;
190 apr_file_seek(fd, APR_SET, &begin);
191
192 /* read a header with metadata */
193 oidc_cache_file_info_t info;
194 if ((rc = oidc_cache_file_read(r, path, fd, &info,
195 sizeof(oidc_cache_file_info_t))) != APR_SUCCESS)
196 goto error_close;
197
198 /* check if this cache entry has already expired */
199 if (apr_time_now() >= info.expire) {
200
201 /* yep, expired: unlock and close before deleting the cache file */
202 apr_file_unlock(fd);
203 apr_file_close(fd);
204
205 /* log this event */
206 oidc_debug(r, "cache entry \"%s\" expired, removing file \"%s\"", key,
207 path);
208
209 /* and kill it */
210 if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) {
211 oidc_error(r, "could not delete cache file \"%s\" (%s)", path,
212 apr_strerror(rc, s_err, sizeof(s_err)));
213 }
214
215 /* nothing strange happened really */
216 return TRUE;
217 }
218
219 /* allocate space for the actual value based on the data size info in the header (+1 for \0 termination) */
220 *value = apr_palloc(r->pool, info.len);
221
222 /* (blocking) read the requested data in to the buffer */
223 rc = oidc_cache_file_read(r, path, fd, (void *) *value, info.len);
224
225 /* barf on failure */
226 if (rc != APR_SUCCESS) {
227 oidc_error(r, "could not read cache value from \"%s\"", path);
228 goto error_close;
229 }
230
231 /* we're done, unlock and close the file */
232 apr_file_unlock(fd);
233 apr_file_close(fd);
234
235 return TRUE;
236
237 error_close:
238
239 apr_file_unlock(fd);
240 apr_file_close(fd);
241
242 oidc_error(r, "return error status %d (%s)", rc,
243 apr_strerror(rc, s_err, sizeof(s_err)));
244
245 return FALSE;
246 }
247
248 // TODO: make these configurable?
249 #define OIDC_CACHE_FILE_LAST_CLEANED "last-cleaned"
250
251 /*
252 * delete all expired entries from the cache directory
253 */
oidc_cache_file_clean(request_rec * r)254 static apr_status_t oidc_cache_file_clean(request_rec *r) {
255 apr_status_t rc = APR_SUCCESS;
256 apr_dir_t *dir = NULL;
257 apr_file_t *fd = NULL;
258 apr_status_t i;
259 apr_finfo_t fi;
260 oidc_cache_file_info_t info;
261 char s_err[128];
262
263 oidc_cfg *cfg = ap_get_module_config(r->server->module_config,
264 &auth_openidc_module);
265
266 /* get the path to the metadata file that holds "last cleaned" metadata info */
267 const char *metadata_path = oidc_cache_file_path(r, "cache-file",
268 OIDC_CACHE_FILE_LAST_CLEANED);
269
270 /* open the metadata file if it exists */
271 if ((rc = apr_stat(&fi, metadata_path, APR_FINFO_MTIME, r->pool))
272 == APR_SUCCESS) {
273
274 /* really only clean once per so much time, check that we haven not recently run */
275 if (apr_time_now() < fi.mtime + apr_time_from_sec(cfg->cache_file_clean_interval)) {
276 oidc_debug(r,
277 "last cleanup call was less than %d seconds ago (next one as early as in %" APR_TIME_T_FMT " secs)",
278 cfg->cache_file_clean_interval,
279 apr_time_sec( fi.mtime + apr_time_from_sec(cfg->cache_file_clean_interval) - apr_time_now()));
280 return APR_SUCCESS;
281 }
282
283 /* time to clean, reset the modification time of the metadata file to reflect the timestamp of this cleaning cycle */
284 apr_file_mtime_set(metadata_path, apr_time_now(), r->pool);
285
286 oidc_debug(r, "start cleaning cycle");
287
288 } else {
289
290 /* no metadata file exists yet, create one (and open it) */
291 if ((rc = apr_file_open(&fd, metadata_path,
292 (APR_FOPEN_WRITE | APR_FOPEN_CREATE), APR_OS_DEFAULT, r->pool))
293 != APR_SUCCESS) {
294 oidc_error(r, "error creating cache timestamp file '%s' (%s)",
295 metadata_path, apr_strerror(rc, s_err, sizeof(s_err)));
296 return rc;
297 }
298
299 /* and cleanup... */
300 if ((rc = apr_file_close(fd)) != APR_SUCCESS) {
301 oidc_error(r, "error closing cache timestamp file '%s' (%s)",
302 metadata_path, apr_strerror(rc, s_err, sizeof(s_err)));
303 }
304 }
305
306 /* time to clean, open the cache directory */
307 if ((rc = apr_dir_open(&dir, cfg->cache_file_dir, r->pool)) != APR_SUCCESS) {
308 oidc_error(r, "error opening cache directory '%s' for cleaning (%s)",
309 cfg->cache_file_dir, apr_strerror(rc, s_err, sizeof(s_err)));
310 return rc;
311 }
312
313 /* loop trough the cache file entries */
314 do {
315
316 /* read the next entry from the directory */
317 i = apr_dir_read(&fi, APR_FINFO_NAME, dir);
318
319 if (i == APR_SUCCESS) {
320
321 /* skip non-cache entries, cq. the ".", ".." and the metadata file */
322 if ((fi.name[0] == OIDC_CHAR_DOT)
323 || (strstr(fi.name, OIDC_CACHE_FILE_PREFIX) != fi.name)
324 || ((apr_strnatcmp(fi.name,
325 oidc_cache_file_name(r, "cache-file",
326 OIDC_CACHE_FILE_LAST_CLEANED)) == 0)))
327 continue;
328
329 /* get the fully qualified path to the cache file and open it */
330 const char *path = apr_psprintf(r->pool, "%s/%s",
331 cfg->cache_file_dir, fi.name);
332 if ((rc = apr_file_open(&fd, path, APR_FOPEN_READ, APR_OS_DEFAULT,
333 r->pool)) != APR_SUCCESS) {
334 oidc_error(r, "unable to open cache entry \"%s\" (%s)", path,
335 apr_strerror(rc, s_err, sizeof(s_err)));
336 continue;
337 }
338
339 /* read the header with cache metadata info */
340 rc = oidc_cache_file_read(r, path, fd, &info,
341 sizeof(oidc_cache_file_info_t));
342 apr_file_close(fd);
343
344 if (rc == APR_SUCCESS) {
345
346 /* check if this entry expired, if not just continue to the next entry */
347 if (apr_time_now() < info.expire)
348 continue;
349
350 /* the cache entry expired, we're going to remove it so log that event */
351 oidc_debug(r, "cache entry (%s) expired, removing file \"%s\")",
352 fi.name, path);
353
354 } else {
355
356 /* file open returned an error, log that */
357 oidc_error(r,
358 "cache entry (%s) corrupted (%s), removing file \"%s\"",
359 fi.name, apr_strerror(rc, s_err, sizeof(s_err)), path);
360
361 }
362
363 /* delete the cache file */
364 if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) {
365
366 /* hrm, this will most probably happen again on the next run... */
367 oidc_error(r, "could not delete cache file \"%s\" (%s)", path,
368 apr_strerror(rc, s_err, sizeof(s_err)));
369 }
370
371 }
372
373 } while (i == APR_SUCCESS);
374
375 apr_dir_close(dir);
376
377 return APR_SUCCESS;
378 }
379
380 /*
381 * write a value for the specified key to the cache
382 */
oidc_cache_file_set(request_rec * r,const char * section,const char * key,const char * value,apr_time_t expiry)383 static apr_byte_t oidc_cache_file_set(request_rec *r, const char *section,
384 const char *key, const char *value, apr_time_t expiry) {
385 apr_file_t *fd = NULL;
386 apr_status_t rc = APR_SUCCESS;
387 char s_err[128];
388
389 /* get the fully qualified path to the cache file based on the key name */
390 const char *path = oidc_cache_file_path(r, section, key);
391
392 /* only on writes (not on reads) we clean the cache first (if not done recently) */
393 oidc_cache_file_clean(r);
394
395 /* just remove cache file if value is NULL */
396 if (value == NULL) {
397 if ((rc = apr_file_remove(path, r->pool)) != APR_SUCCESS) {
398 oidc_error(r, "could not delete cache file \"%s\" (%s)", path,
399 apr_strerror(rc, s_err, sizeof(s_err)));
400 }
401 return TRUE;
402 }
403
404 /* try to open the cache file for writing, creating it if it does not exist */
405 if ((rc = apr_file_open(&fd, path,
406 (APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_TRUNCATE),
407 APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) {
408 oidc_error(r, "cache file \"%s\" could not be opened (%s)", path,
409 apr_strerror(rc, s_err, sizeof(s_err)));
410 return FALSE;
411 }
412
413 /* lock the file and move the write pointer to the start of it */
414 apr_file_lock(fd, APR_FLOCK_EXCLUSIVE);
415 apr_off_t begin = 0;
416 apr_file_seek(fd, APR_SET, &begin);
417
418 /* construct the metadata for this cache entry in the header info */
419 oidc_cache_file_info_t info;
420 info.expire = expiry;
421 info.len = strlen(value) + 1;
422
423 /* write the header */
424 if ((rc = oidc_cache_file_write(r, path, fd, &info,
425 sizeof(oidc_cache_file_info_t))) != APR_SUCCESS)
426 return FALSE;
427
428 /* next write the value */
429 rc = oidc_cache_file_write(r, path, fd, (void *) value, info.len);
430
431 /* unlock and close the written file */
432 apr_file_unlock(fd);
433 apr_file_close(fd);
434
435 /* log our success/failure */
436 oidc_debug(r,
437 "%s entry for key \"%s\" in file of %" APR_SIZE_T_FMT " bytes",
438 (rc == APR_SUCCESS) ? "successfully stored" : "could not store",
439 key, info.len);
440
441 return (rc == APR_SUCCESS);
442 }
443
444 oidc_cache_t oidc_cache_file = {
445 "file",
446 1,
447 oidc_cache_file_post_config,
448 NULL,
449 oidc_cache_file_get,
450 oidc_cache_file_set,
451 NULL
452 };
453