]> git.ipfire.org Git - thirdparty/squid.git/blame - helpers/external_acl/time_quota/ext_time_quota_acl.cc
SourceFormat Enforcement
[thirdparty/squid.git] / helpers / external_acl / time_quota / ext_time_quota_acl.cc
CommitLineData
9938b57f
TB
1/*
2 * ext_time_quota_acl: Squid external acl helper for quota on usage.
3 *
4 * Copyright (C) 2011 Dr. Tilmann Bubeck <t.bubeck@reinform.de>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
19 */
20
21#if HAVE_CONFIG_H
f7f3304a 22#include "squid.h"
9938b57f
TB
23#endif
24#include "helpers/defines.h"
25
26#include <sys/types.h>
27#include <sys/stat.h>
28#include <fcntl.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <stdarg.h>
32#if HAVE_UNISTD_H
33#include <unistd.h>
34#endif
35#include <string.h>
36#include <time.h>
37#if HAVE_GETOPT_H
38#include <getopt.h>
39#endif
40
41/* At this point all Bit Types are already defined, so we must
42 protect from multiple type definition on platform where
43 __BIT_TYPES_DEFINED__ is not defined.
44 */
45#ifndef __BIT_TYPES_DEFINED__
46#define __BIT_TYPES_DEFINED__
47#endif
48
49#if HAVE_DB_185_H
50#include <db_185.h>
51#elif HAVE_DB_H
52#include <db.h>
53#endif
54
55#ifndef DEFAULT_QUOTA_DB
56#error "Please define DEFAULT_QUOTA_DB preprocessor constant."
57#endif
58
59const char *db_path = DEFAULT_QUOTA_DB;
60const char *program_name;
61
62DB *db = NULL;
63
64#define KEY_LAST_ACTIVITY "last-activity"
65#define KEY_PERIOD_START "period-start"
66#define KEY_PERIOD_LENGTH_CONFIGURED "period-length-configured"
67#define KEY_TIME_BUDGET_LEFT "time-budget-left"
68#define KEY_TIME_BUDGET_CONFIGURED "time-budget-configured"
69
70/** Maximum size of buffers used to read or display lines. */
71#define TQ_BUFFERSIZE 1024
72
73/** If there is more than this given number of seconds between two
f8901ea9 74 * sucessive requests, than the second request will be treated as a
9938b57f
TB
75 * new request and the time between first and seconds request will
76 * be treated as a activity pause.
77 *
78 * Otherwise the following request will be treated as belonging to the
79 * same activity and the quota will be reduced.
80 */
81static int pauseLength = 300;
82
83static FILE *logfile = stderr;
84static int tq_debug_enabled = false;
85
f8901ea9
A
86static void open_log(const char *logfilename)
87{
9938b57f
TB
88 logfile = fopen(logfilename, "a");
89 if ( logfile == NULL ) {
f8901ea9
A
90 perror(logfilename);
91 logfile = stderr;
9938b57f
TB
92 }
93}
94
95static void vlog(const char *level, const char *format, va_list args)
96{
97 time_t now = time(NULL);
98
3876fdac
A
99 fprintf(logfile, "%ld %s| %s: ", static_cast<long int>(now),
100 program_name, level);
9938b57f
TB
101 vfprintf (logfile, format, args);
102 fflush(logfile);
103}
104
105static void log_debug(const char *format, ...)
106{
107 va_list args;
108
109 if ( tq_debug_enabled ) {
f8901ea9
A
110 va_start (args, format);
111 vlog("DEBUG", format, args);
112 va_end (args);
9938b57f
TB
113 }
114}
115
116static void log_info(const char *format, ...)
117{
118 va_list args;
119
120 va_start (args, format);
121 vlog("INFO", format, args);
122 va_end (args);
123}
124
125static void log_error(const char *format, ...)
126{
127 va_list args;
128
129 va_start (args, format);
130 vlog("ERROR", format, args);
131 va_end (args);
132}
133
134static void log_fatal(const char *format, ...)
135{
136 va_list args;
137
138 va_start (args, format);
139 vlog("FATAL", format, args);
140 va_end (args);
141}
142
143static void init_db(void)
144{
145 log_info("opening time quota database \"%s\".\n", db_path);
146 db = dbopen(db_path, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL);
147 if (!db) {
148 log_fatal("Failed to open time_quota db '%s'\n", db_path);
149 exit(1);
150 }
151}
152
153static void shutdown_db(void)
154{
155 db->close(db);
156}
157
158static void writeTime(const char *user_key, const char *sub_key, time_t t)
159{
160 char keybuffer[TQ_BUFFERSIZE];
161 DBT key, data;
162
163 if ( strlen(user_key) + strlen(sub_key) + 1 + 1 > sizeof(keybuffer) ) {
f8901ea9 164 log_error("key too long (%s,%s)\n", user_key, sub_key);
9938b57f 165 } else {
f8901ea9
A
166 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
167
168 key.data = (void *)keybuffer;
169 key.size = strlen(keybuffer);
170 data.data = &t;
171 data.size = sizeof(t);
172 db->put(db, &key, &data, 0);
173 log_debug("writeTime(\"%s\", %d)\n", keybuffer, t);
9938b57f
TB
174 }
175}
176
177static time_t readTime(const char *user_key, const char *sub_key)
178{
179 char keybuffer[TQ_BUFFERSIZE];
180 DBT key, data;
181 time_t t = 0;
182
183 if ( strlen(user_key) + 1 + strlen(sub_key) + 1 > sizeof(keybuffer) ) {
f8901ea9 184 log_error("key too long (%s,%s)\n", user_key, sub_key);
9938b57f 185 } else {
f8901ea9
A
186 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
187
188 key.data = (void *)keybuffer;
189 key.size = strlen(keybuffer);
190 if (db->get(db, &key, &data, 0) == 0) {
191 if (data.size != sizeof(t)) {
192 log_error("CORRUPTED DATABASE (%s)\n", keybuffer);
193 } else {
194 memcpy(&t, data.data, sizeof(t));
195 }
196 }
197 log_debug("readTime(\"%s\")=%d\n", keybuffer, t);
9938b57f
TB
198 }
199
200 return t;
201}
202
203static void parseTime(const char *s, time_t *secs, time_t *start)
204{
205 double value;
206 char unit;
207 struct tm *ltime;
208 int periodLength = 3600;
209
210 *secs = 0;
211 *start = time(NULL);
212 ltime = localtime(start);
213
214 sscanf(s, " %lf %c", &value, &unit);
215 switch (unit) {
216 case 's':
f8901ea9
A
217 periodLength = 1;
218 break;
9938b57f 219 case 'm':
f8901ea9
A
220 periodLength = 60;
221 *start -= ltime->tm_sec;
222 break;
9938b57f 223 case 'h':
f8901ea9
A
224 periodLength = 3600;
225 *start -= ltime->tm_min * 60 + ltime->tm_sec;
226 break;
9938b57f 227 case 'd':
f8901ea9
A
228 periodLength = 24 * 3600;
229 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
230 break;
9938b57f 231 case 'w':
f8901ea9
A
232 periodLength = 7 * 24 * 3600;
233 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
234 *start -= ltime->tm_wday * 24 * 3600;
235 *start += 24 * 3600; // in europe, the week starts monday
236 break;
9938b57f 237 default:
f8901ea9
A
238 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit);
239 break;
9938b57f
TB
240 }
241
242 *secs = (long)(periodLength * value);
243}
244
9938b57f
TB
245/** This function parses the time quota file and stores it
246 * in memory.
247 */
f8901ea9 248static void readConfig(const char *filename)
9938b57f
TB
249{
250 char line[TQ_BUFFERSIZE]; /* the buffer for the lines read
251 from the dict file */
252 char *cp; /* a char pointer used to parse
253 each line */
254 char *username; /* for the username */
255 char *budget;
256 char *period;
257 FILE *FH;
258 time_t t;
259 time_t budgetSecs, periodSecs;
260 time_t start;
261
262 log_info("reading config file \"%s\".\n", filename);
263
264 FH = fopen(filename, "r");
265 if ( FH ) {
f8901ea9
A
266 /* the pointer to the first entry in the linked list */
267 while ((cp = fgets (line, sizeof(line), FH)) != NULL) {
268 if (line[0] == '#') {
269 continue;
270 }
271 if ((cp = strchr (line, '\n')) != NULL) {
272 /* chop \n characters */
273 *cp = '\0';
274 }
275 log_debug("read config line \"%s\".\n", line);
276 if ((cp = strtok (line, "\t ")) != NULL) {
277 username = cp;
278
279 /* get the time budget */
280 budget = strtok (NULL, "/");
281 period = strtok (NULL, "/");
282
283 parseTime(budget, &budgetSecs, &start);
284 parseTime(period, &periodSecs, &start);
285
286 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
287 username, budgetSecs, periodSecs, start);
288
289 writeTime(username, KEY_PERIOD_START, start);
290 writeTime(username, KEY_PERIOD_LENGTH_CONFIGURED, periodSecs);
291 writeTime(username, KEY_TIME_BUDGET_CONFIGURED, budgetSecs);
292 t = readTime(username, KEY_TIME_BUDGET_CONFIGURED);
293 writeTime(username, KEY_TIME_BUDGET_LEFT, t);
294 }
295 }
296 fclose(FH);
9938b57f 297 } else {
f8901ea9 298 perror(filename);
9938b57f
TB
299 }
300}
301
302static void processActivity(const char *user_key)
303{
f8901ea9
A
304 time_t now = time(NULL);
305 time_t lastActivity;
306 time_t activityLength;
307 time_t periodStart;
308 time_t periodLength;
309 time_t userPeriodLength;
310 time_t timeBudgetCurrent;
311 time_t timeBudgetConfigured;
312 char message[TQ_BUFFERSIZE];
313
314 log_debug("processActivity(\"%s\")\n", user_key);
315
316 // [1] Reset period if over
317 periodStart = readTime(user_key, KEY_PERIOD_START);
318 if ( periodStart == 0 ) {
319 // This is the first period ever.
320 periodStart = now;
321 writeTime(user_key, KEY_PERIOD_START, periodStart);
322 }
323
324 periodLength = now - periodStart;
325 userPeriodLength = readTime(user_key, KEY_PERIOD_LENGTH_CONFIGURED);
326 if ( userPeriodLength == 0 ) {
327 // This user is not configured. Allow anything.
328 log_debug("No period length found for user \"%s\". Quota for this user disabled.\n", user_key);
329 writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
330 } else {
331 if ( periodLength >= userPeriodLength ) {
332 // a new period has started.
333 log_debug("New time period started for user \"%s\".\n", user_key);
334 while ( periodStart < now ) {
335 periodStart += periodLength;
336 }
337 writeTime(user_key, KEY_PERIOD_START, periodStart);
338 timeBudgetConfigured = readTime(user_key, KEY_TIME_BUDGET_CONFIGURED);
339 if ( timeBudgetConfigured == 0 ) {
340 log_debug("No time budget configured for user \"%s\". Quota for this user disabled.\n", user_key);
341 writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
342 } else {
343 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetConfigured);
344 }
345 }
346 }
347
348 // [2] Decrease time budget iff activity
349 lastActivity = readTime(user_key, KEY_LAST_ACTIVITY);
350 if ( lastActivity == 0 ) {
351 // This is the first request ever
352 writeTime(user_key, KEY_LAST_ACTIVITY, now);
353 } else {
354 activityLength = now - lastActivity;
355 if ( activityLength >= pauseLength ) {
356 // This is an activity pause.
357 log_debug("Activity pause detected for user \"%s\".\n", user_key);
358 writeTime(user_key, KEY_LAST_ACTIVITY, now);
359 } else {
360 // This is real usage.
361 writeTime(user_key, KEY_LAST_ACTIVITY, now);
362
363 log_debug("Time budget reduced by %ld for user \"%s\".\n",
364 activityLength, user_key);
365 timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
366 timeBudgetCurrent -= activityLength;
367 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetCurrent);
368 }
369 }
370
371 timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
372 snprintf(message, TQ_BUFFERSIZE, "message=\"Remaining quota for '%s' is %d seconds.\"", user_key, (int)timeBudgetCurrent);
373 if ( timeBudgetCurrent > 0 ) {
374 log_debug("OK %s.\n", message);
375 SEND_OK(message);
376 } else {
377 log_debug("ERR %s\n", message);
378 SEND_ERR("Time budget exceeded.");
379 }
380
381 db->sync(db, 0);
9938b57f
TB
382}
383
384static void usage(void)
385{
386 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
387
388 fprintf(stderr, "Usage: %s [-d] [-l logfile] [-b dbpath] [-p pauselen] [-h] configfile\n", program_name);
389 fprintf(stderr, " -d enable debugging output to logfile\n");
390 fprintf(stderr, " -l logfile log messages to logfile\n");
391 fprintf(stderr, " -b dbpath Path where persistent session database will be kept\n");
392 fprintf(stderr, " If option is not used, then " DEFAULT_QUOTA_DB " will be used.\n");
393 fprintf(stderr, " -p pauselen length in seconds to describe a pause between 2 requests.\n");
394 fprintf(stderr, " -h show show command line help.\n");
395 fprintf(stderr, "configfile is a file containing time quota definitions.\n");
396}
397
398int main(int argc, char **argv)
399{
400 char request[HELPER_INPUT_BUFFER];
401 int opt;
402
403 program_name = argv[0];
404
405 while ((opt = getopt(argc, argv, "dp:l:b:h")) != -1) {
406 switch (opt) {
407 case 'd':
f8901ea9 408 tq_debug_enabled = true;
9938b57f
TB
409 break;
410 case 'l':
f8901ea9 411 open_log(optarg);
9938b57f
TB
412 break;
413 case 'b':
414 db_path = optarg;
415 break;
416 case 'p':
417 pauseLength = atoi(optarg);
418 break;
419 case 'h':
420 usage();
421 exit(0);
422 break;
423 }
424 }
425
426 log_info("Starting %s\n", __FILE__);
427 setbuf(stdout, NULL);
428
429 init_db();
430
431 if ( optind + 1 != argc ) {
f8901ea9
A
432 usage();
433 exit(1);
9938b57f 434 } else {
f8901ea9 435 readConfig(argv[optind]);
9938b57f
TB
436 }
437
438 log_info("Waiting for requests...\n");
439 while (fgets(request, HELPER_INPUT_BUFFER, stdin)) {
f8901ea9 440 // we expect the following line syntax: "%LOGIN
9938b57f
TB
441 const char *user_key = NULL;
442 user_key = strtok(request, " \n");
443
f8901ea9 444 processActivity(user_key);
9938b57f
TB
445 }
446 log_info("Ending %s\n", __FILE__);
447 shutdown_db();
448 return 0;
449}