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