]> git.ipfire.org Git - thirdparty/squid.git/blame - helpers/external_acl/time_quota/ext_time_quota_acl.cc
Add ext_time_quota_acl helper
[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
22#include "config.h"
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
74 * sucessive requests, than the second request will be treated as a
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
86static void open_log(const char *logfilename) {
87 logfile = fopen(logfilename, "a");
88 if ( logfile == NULL ) {
89 perror(logfilename);
90 logfile = stderr;
91 }
92}
93
94static void vlog(const char *level, const char *format, va_list args)
95{
96 time_t now = time(NULL);
97
98 fprintf(logfile, "%ld %s| %s: ", now, program_name, level);
99 vfprintf (logfile, format, args);
100 fflush(logfile);
101}
102
103static void log_debug(const char *format, ...)
104{
105 va_list args;
106
107 if ( tq_debug_enabled ) {
108 va_start (args, format);
109 vlog("DEBUG", format, args);
110 va_end (args);
111 }
112}
113
114static void log_info(const char *format, ...)
115{
116 va_list args;
117
118 va_start (args, format);
119 vlog("INFO", format, args);
120 va_end (args);
121}
122
123static void log_error(const char *format, ...)
124{
125 va_list args;
126
127 va_start (args, format);
128 vlog("ERROR", format, args);
129 va_end (args);
130}
131
132static void log_fatal(const char *format, ...)
133{
134 va_list args;
135
136 va_start (args, format);
137 vlog("FATAL", format, args);
138 va_end (args);
139}
140
141static void init_db(void)
142{
143 log_info("opening time quota database \"%s\".\n", db_path);
144 db = dbopen(db_path, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL);
145 if (!db) {
146 log_fatal("Failed to open time_quota db '%s'\n", db_path);
147 exit(1);
148 }
149}
150
151static void shutdown_db(void)
152{
153 db->close(db);
154}
155
156static void writeTime(const char *user_key, const char *sub_key, time_t t)
157{
158 char keybuffer[TQ_BUFFERSIZE];
159 DBT key, data;
160
161 if ( strlen(user_key) + strlen(sub_key) + 1 + 1 > sizeof(keybuffer) ) {
162 log_error("key too long (%s,%s)\n", user_key, sub_key);
163 } else {
164 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
165
166 key.data = (void *)keybuffer;
167 key.size = strlen(keybuffer);
168 data.data = &t;
169 data.size = sizeof(t);
170 db->put(db, &key, &data, 0);
171 log_debug("writeTime(\"%s\", %d)\n", keybuffer, t);
172 }
173}
174
175static time_t readTime(const char *user_key, const char *sub_key)
176{
177 char keybuffer[TQ_BUFFERSIZE];
178 DBT key, data;
179 time_t t = 0;
180
181 if ( strlen(user_key) + 1 + strlen(sub_key) + 1 > sizeof(keybuffer) ) {
182 log_error("key too long (%s,%s)\n", user_key, sub_key);
183 } else {
184 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
185
186 key.data = (void *)keybuffer;
187 key.size = strlen(keybuffer);
188 if (db->get(db, &key, &data, 0) == 0) {
189 if (data.size != sizeof(t)) {
190 log_error("CORRUPTED DATABASE (%s)\n", keybuffer);
191 } else {
192 memcpy(&t, data.data, sizeof(t));
193 }
194 }
195 log_debug("readTime(\"%s\")=%d\n", keybuffer, t);
196 }
197
198 return t;
199}
200
201static void parseTime(const char *s, time_t *secs, time_t *start)
202{
203 double value;
204 char unit;
205 struct tm *ltime;
206 int periodLength = 3600;
207
208 *secs = 0;
209 *start = time(NULL);
210 ltime = localtime(start);
211
212 sscanf(s, " %lf %c", &value, &unit);
213 switch (unit) {
214 case 's':
215 periodLength = 1;
216 break;
217 case 'm':
218 periodLength = 60;
219 *start -= ltime->tm_sec;
220 break;
221 case 'h':
222 periodLength = 3600;
223 *start -= ltime->tm_min * 60 + ltime->tm_sec;
224 break;
225 case 'd':
226 periodLength = 24 * 3600;
227 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
228 break;
229 case 'w':
230 periodLength = 7 * 24 * 3600;
231 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
232 *start -= ltime->tm_wday * 24 * 3600;
233 *start += 24 * 3600; // in europe, the week starts monday
234 break;
235 default:
236 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit);
237 break;
238 }
239
240 *secs = (long)(periodLength * value);
241}
242
243
244/** This function parses the time quota file and stores it
245 * in memory.
246 */
247static void readConfig(const char *filename)
248{
249 char line[TQ_BUFFERSIZE]; /* the buffer for the lines read
250 from the dict file */
251 char *cp; /* a char pointer used to parse
252 each line */
253 char *username; /* for the username */
254 char *budget;
255 char *period;
256 FILE *FH;
257 time_t t;
258 time_t budgetSecs, periodSecs;
259 time_t start;
260
261 log_info("reading config file \"%s\".\n", filename);
262
263 FH = fopen(filename, "r");
264 if ( FH ) {
265 /* the pointer to the first entry in the linked list */
266 while ((cp = fgets (line, sizeof(line), FH)) != NULL) {
267 if (line[0] == '#') {
268 continue;
269 }
270 if ((cp = strchr (line, '\n')) != NULL) {
271 /* chop \n characters */
272 *cp = '\0';
273 }
274 log_debug("read config line \"%s\".\n", line);
275 if ((cp = strtok (line, "\t ")) != NULL) {
276 username = cp;
277
278 /* get the time budget */
279 budget = strtok (NULL, "/");
280 period = strtok (NULL, "/");
281
282 parseTime(budget, &budgetSecs, &start);
283 parseTime(period, &periodSecs, &start);
284
285 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
286 username, budgetSecs, periodSecs, start);
287
288 writeTime(username, KEY_PERIOD_START, start);
289 writeTime(username, KEY_PERIOD_LENGTH_CONFIGURED, periodSecs);
290 writeTime(username, KEY_TIME_BUDGET_CONFIGURED, budgetSecs);
291 t = readTime(username, KEY_TIME_BUDGET_CONFIGURED);
292 writeTime(username, KEY_TIME_BUDGET_LEFT, t);
293 }
294 }
295 fclose(FH);
296 } else {
297 perror(filename);
298 }
299}
300
301static void processActivity(const char *user_key)
302{
303 time_t now = time(NULL);
304 time_t lastActivity;
305 time_t activityLength;
306 time_t periodStart;
307 time_t periodLength;
308 time_t userPeriodLength;
309 time_t timeBudgetCurrent;
310 time_t timeBudgetConfigured;
311 char message[TQ_BUFFERSIZE];
312
313 log_debug("processActivity(\"%s\")\n", user_key);
314
315 // [1] Reset period if over
316 periodStart = readTime(user_key, KEY_PERIOD_START);
317 if ( periodStart == 0 ) {
318 // This is the first period ever.
319 periodStart = now;
320 writeTime(user_key, KEY_PERIOD_START, periodStart);
321 }
322
323 periodLength = now - periodStart;
324 userPeriodLength = readTime(user_key, KEY_PERIOD_LENGTH_CONFIGURED);
325 if ( userPeriodLength == 0 ) {
326 // This user is not configured. Allow anything.
327 log_debug("No period length found for user \"%s\". Quota for this user disabled.\n", user_key);
328 writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
329 } else {
330 if ( periodLength >= userPeriodLength ) {
331 // a new period has started.
332 log_debug("New time period started for user \"%s\".\n", user_key);
333 while ( periodStart < now ) {
334 periodStart += periodLength;
335 }
336 writeTime(user_key, KEY_PERIOD_START, periodStart);
337 timeBudgetConfigured = readTime(user_key, KEY_TIME_BUDGET_CONFIGURED);
338 if ( timeBudgetConfigured == 0 ) {
339 log_debug("No time budget configured for user \"%s\". Quota for this user disabled.\n", user_key);
340 writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
341 } else {
342 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetConfigured);
343 }
344 }
345 }
346
347 // [2] Decrease time budget iff activity
348 lastActivity = readTime(user_key, KEY_LAST_ACTIVITY);
349 if ( lastActivity == 0 ) {
350 // This is the first request ever
351 writeTime(user_key, KEY_LAST_ACTIVITY, now);
352 } else {
353 activityLength = now - lastActivity;
354 if ( activityLength >= pauseLength ) {
355 // This is an activity pause.
356 log_debug("Activity pause detected for user \"%s\".\n", user_key);
357 writeTime(user_key, KEY_LAST_ACTIVITY, now);
358 } else {
359 // This is real usage.
360 writeTime(user_key, KEY_LAST_ACTIVITY, now);
361
362 log_debug("Time budget reduced by %ld for user \"%s\".\n",
363 activityLength, user_key);
364 timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
365 timeBudgetCurrent -= activityLength;
366 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetCurrent);
367 }
368 }
369
370 timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
371 snprintf(message, TQ_BUFFERSIZE, "message=\"Remaining quota for '%s' is %d seconds.\"", user_key, (int)timeBudgetCurrent);
372 if ( timeBudgetCurrent > 0 ) {
373 log_debug("OK %s.\n", message);
374 SEND_OK(message);
375 } else {
376 log_debug("ERR %s\n", message);
377 SEND_ERR("Time budget exceeded.");
378 }
379
380 db->sync(db, 0);
381}
382
383static void usage(void)
384{
385 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
386
387 fprintf(stderr, "Usage: %s [-d] [-l logfile] [-b dbpath] [-p pauselen] [-h] configfile\n", program_name);
388 fprintf(stderr, " -d enable debugging output to logfile\n");
389 fprintf(stderr, " -l logfile log messages to logfile\n");
390 fprintf(stderr, " -b dbpath Path where persistent session database will be kept\n");
391 fprintf(stderr, " If option is not used, then " DEFAULT_QUOTA_DB " will be used.\n");
392 fprintf(stderr, " -p pauselen length in seconds to describe a pause between 2 requests.\n");
393 fprintf(stderr, " -h show show command line help.\n");
394 fprintf(stderr, "configfile is a file containing time quota definitions.\n");
395}
396
397int main(int argc, char **argv)
398{
399 char request[HELPER_INPUT_BUFFER];
400 int opt;
401
402 program_name = argv[0];
403
404 while ((opt = getopt(argc, argv, "dp:l:b:h")) != -1) {
405 switch (opt) {
406 case 'd':
407 tq_debug_enabled = true;
408 break;
409 case 'l':
410 open_log(optarg);
411 break;
412 case 'b':
413 db_path = optarg;
414 break;
415 case 'p':
416 pauseLength = atoi(optarg);
417 break;
418 case 'h':
419 usage();
420 exit(0);
421 break;
422 }
423 }
424
425 log_info("Starting %s\n", __FILE__);
426 setbuf(stdout, NULL);
427
428 init_db();
429
430 if ( optind + 1 != argc ) {
431 usage();
432 exit(1);
433 } else {
434 readConfig(argv[optind]);
435 }
436
437 log_info("Waiting for requests...\n");
438 while (fgets(request, HELPER_INPUT_BUFFER, stdin)) {
439 // we expect the following line syntax: "%LOGIN
440 const char *user_key = NULL;
441 user_key = strtok(request, " \n");
442
443 processActivity(user_key);
444 }
445 log_info("Ending %s\n", __FILE__);
446 shutdown_db();
447 return 0;
448}