2 * Copyright (C) 1996-2015 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
10 * ext_time_quota_acl: Squid external acl helper for quota on usage.
12 * Copyright (C) 2011 Dr. Tilmann Bubeck <t.bubeck@reinform.de>
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 2 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
30 #include "helpers/defines.h"
36 #include <sys/types.h>
46 /* At this point all Bit Types are already defined, so we must
47 protect from multiple type definition on platform where
48 __BIT_TYPES_DEFINED__ is not defined.
50 #ifndef __BIT_TYPES_DEFINED__
51 #define __BIT_TYPES_DEFINED__
60 #ifndef DEFAULT_QUOTA_DB
61 #error "Please define DEFAULT_QUOTA_DB preprocessor constant."
64 const char *db_path
= DEFAULT_QUOTA_DB
;
65 const char *program_name
;
69 #define KEY_LAST_ACTIVITY "last-activity"
70 #define KEY_PERIOD_START "period-start"
71 #define KEY_PERIOD_LENGTH_CONFIGURED "period-length-configured"
72 #define KEY_TIME_BUDGET_LEFT "time-budget-left"
73 #define KEY_TIME_BUDGET_CONFIGURED "time-budget-configured"
75 /** Maximum size of buffers used to read or display lines. */
76 #define TQ_BUFFERSIZE 1024
78 /** If there is more than this given number of seconds between two
79 * sucessive requests, than the second request will be treated as a
80 * new request and the time between first and seconds request will
81 * be treated as a activity pause.
83 * Otherwise the following request will be treated as belonging to the
84 * same activity and the quota will be reduced.
86 static int pauseLength
= 300;
88 static FILE *logfile
= stderr
;
89 static int tq_debug_enabled
= false;
91 static void open_log(const char *logfilename
)
93 logfile
= fopen(logfilename
, "a");
94 if ( logfile
== NULL
) {
100 static void vlog(const char *level
, const char *format
, va_list args
)
102 time_t now
= time(NULL
);
104 fprintf(logfile
, "%ld %s| %s: ", static_cast<long int>(now
),
105 program_name
, level
);
106 vfprintf (logfile
, format
, args
);
110 static void log_debug(const char *format
, ...)
114 if ( tq_debug_enabled
) {
115 va_start (args
, format
);
116 vlog("DEBUG", format
, args
);
121 static void log_info(const char *format
, ...)
125 va_start (args
, format
);
126 vlog("INFO", format
, args
);
130 static void log_error(const char *format
, ...)
134 va_start (args
, format
);
135 vlog("ERROR", format
, args
);
139 static void log_fatal(const char *format
, ...)
143 va_start (args
, format
);
144 vlog("FATAL", format
, args
);
148 static void init_db(void)
150 log_info("opening time quota database \"%s\".\n", db_path
);
151 db
= dbopen(db_path
, O_CREAT
| O_RDWR
, 0666, DB_BTREE
, NULL
);
153 log_fatal("Failed to open time_quota db '%s'\n", db_path
);
158 static void shutdown_db(void)
163 static void writeTime(const char *user_key
, const char *sub_key
, time_t t
)
165 char keybuffer
[TQ_BUFFERSIZE
];
168 if ( strlen(user_key
) + strlen(sub_key
) + 1 + 1 > sizeof(keybuffer
) ) {
169 log_error("key too long (%s,%s)\n", user_key
, sub_key
);
171 snprintf(keybuffer
, sizeof(keybuffer
), "%s-%s", user_key
, sub_key
);
173 key
.data
= (void *)keybuffer
;
174 key
.size
= strlen(keybuffer
);
176 data
.size
= sizeof(t
);
177 db
->put(db
, &key
, &data
, 0);
178 log_debug("writeTime(\"%s\", %d)\n", keybuffer
, t
);
182 static time_t readTime(const char *user_key
, const char *sub_key
)
184 char keybuffer
[TQ_BUFFERSIZE
];
188 if ( strlen(user_key
) + 1 + strlen(sub_key
) + 1 > sizeof(keybuffer
) ) {
189 log_error("key too long (%s,%s)\n", user_key
, sub_key
);
191 snprintf(keybuffer
, sizeof(keybuffer
), "%s-%s", user_key
, sub_key
);
193 key
.data
= (void *)keybuffer
;
194 key
.size
= strlen(keybuffer
);
195 if (db
->get(db
, &key
, &data
, 0) == 0) {
196 if (data
.size
!= sizeof(t
)) {
197 log_error("CORRUPTED DATABASE (%s)\n", keybuffer
);
199 memcpy(&t
, data
.data
, sizeof(t
));
202 log_debug("readTime(\"%s\")=%d\n", keybuffer
, t
);
208 static void parseTime(const char *s
, time_t *secs
, time_t *start
)
213 int periodLength
= 3600;
217 ltime
= localtime(start
);
219 sscanf(s
, " %lf %c", &value
, &unit
);
226 *start
-= ltime
->tm_sec
;
230 *start
-= ltime
->tm_min
* 60 + ltime
->tm_sec
;
233 periodLength
= 24 * 3600;
234 *start
-= ltime
->tm_hour
* 3600 + ltime
->tm_min
* 60 + ltime
->tm_sec
;
237 periodLength
= 7 * 24 * 3600;
238 *start
-= ltime
->tm_hour
* 3600 + ltime
->tm_min
* 60 + ltime
->tm_sec
;
239 *start
-= ltime
->tm_wday
* 24 * 3600;
240 *start
+= 24 * 3600; // in europe, the week starts monday
243 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit
);
247 *secs
= (long)(periodLength
* value
);
250 /** This function parses the time quota file and stores it
253 static void readConfig(const char *filename
)
255 char line
[TQ_BUFFERSIZE
]; /* the buffer for the lines read
256 from the dict file */
257 char *cp
; /* a char pointer used to parse
259 char *username
; /* for the username */
264 time_t budgetSecs
, periodSecs
;
267 log_info("reading config file \"%s\".\n", filename
);
269 FH
= fopen(filename
, "r");
271 /* the pointer to the first entry in the linked list */
272 unsigned int lineCount
= 0;
273 while (fgets(line
, sizeof(line
), FH
)) {
275 if (line
[0] == '#') {
278 if ((cp
= strchr (line
, '\n')) != NULL
) {
279 /* chop \n characters */
282 log_debug("read config line %u: \"%s\".\n", lineCount
, line
);
283 if ((username
= strtok(line
, "\t ")) != NULL
) {
285 /* get the time budget */
286 if ((budget
= strtok(NULL
, "/")) == NULL
) {
287 fprintf(stderr
, "ERROR: missing 'budget' field on line %u of '%s'.\n", lineCount
, filename
);
290 if ((period
= strtok(NULL
, "/")) == NULL
) {
291 fprintf(stderr
, "ERROR: missing 'period' field on line %u of '%s'.\n", lineCount
, filename
);
295 parseTime(budget
, &budgetSecs
, &start
);
296 parseTime(period
, &periodSecs
, &start
);
298 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
299 username
, budgetSecs
, periodSecs
, start
);
301 writeTime(username
, KEY_PERIOD_START
, start
);
302 writeTime(username
, KEY_PERIOD_LENGTH_CONFIGURED
, periodSecs
);
303 writeTime(username
, KEY_TIME_BUDGET_CONFIGURED
, budgetSecs
);
304 t
= readTime(username
, KEY_TIME_BUDGET_CONFIGURED
);
305 writeTime(username
, KEY_TIME_BUDGET_LEFT
, t
);
314 static void processActivity(const char *user_key
)
316 time_t now
= time(NULL
);
318 time_t activityLength
;
321 time_t userPeriodLength
;
322 time_t timeBudgetCurrent
;
323 time_t timeBudgetConfigured
;
324 char message
[TQ_BUFFERSIZE
];
326 log_debug("processActivity(\"%s\")\n", user_key
);
328 // [1] Reset period if over
329 periodStart
= readTime(user_key
, KEY_PERIOD_START
);
330 if ( periodStart
== 0 ) {
331 // This is the first period ever.
333 writeTime(user_key
, KEY_PERIOD_START
, periodStart
);
336 periodLength
= now
- periodStart
;
337 userPeriodLength
= readTime(user_key
, KEY_PERIOD_LENGTH_CONFIGURED
);
338 if ( userPeriodLength
== 0 ) {
339 // This user is not configured. Allow anything.
340 log_debug("No period length found for user \"%s\". Quota for this user disabled.\n", user_key
);
341 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, pauseLength
);
343 if ( periodLength
>= userPeriodLength
) {
344 // a new period has started.
345 log_debug("New time period started for user \"%s\".\n", user_key
);
346 while ( periodStart
< now
) {
347 periodStart
+= periodLength
;
349 writeTime(user_key
, KEY_PERIOD_START
, periodStart
);
350 timeBudgetConfigured
= readTime(user_key
, KEY_TIME_BUDGET_CONFIGURED
);
351 if ( timeBudgetConfigured
== 0 ) {
352 log_debug("No time budget configured for user \"%s\". Quota for this user disabled.\n", user_key
);
353 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, pauseLength
);
355 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, timeBudgetConfigured
);
360 // [2] Decrease time budget iff activity
361 lastActivity
= readTime(user_key
, KEY_LAST_ACTIVITY
);
362 if ( lastActivity
== 0 ) {
363 // This is the first request ever
364 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
366 activityLength
= now
- lastActivity
;
367 if ( activityLength
>= pauseLength
) {
368 // This is an activity pause.
369 log_debug("Activity pause detected for user \"%s\".\n", user_key
);
370 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
372 // This is real usage.
373 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
375 log_debug("Time budget reduced by %ld for user \"%s\".\n",
376 activityLength
, user_key
);
377 timeBudgetCurrent
= readTime(user_key
, KEY_TIME_BUDGET_LEFT
);
378 timeBudgetCurrent
-= activityLength
;
379 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, timeBudgetCurrent
);
383 timeBudgetCurrent
= readTime(user_key
, KEY_TIME_BUDGET_LEFT
);
384 snprintf(message
, TQ_BUFFERSIZE
, "message=\"Remaining quota for '%s' is %d seconds.\"", user_key
, (int)timeBudgetCurrent
);
385 if ( timeBudgetCurrent
> 0 ) {
386 log_debug("OK %s.\n", message
);
389 log_debug("ERR %s\n", message
);
390 SEND_ERR("Time budget exceeded.");
396 static void usage(void)
398 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
400 fprintf(stderr
, "Usage: %s [-d] [-l logfile] [-b dbpath] [-p pauselen] [-h] configfile\n", program_name
);
401 fprintf(stderr
, " -d enable debugging output to logfile\n");
402 fprintf(stderr
, " -l logfile log messages to logfile\n");
403 fprintf(stderr
, " -b dbpath Path where persistent session database will be kept\n");
404 fprintf(stderr
, " If option is not used, then " DEFAULT_QUOTA_DB
" will be used.\n");
405 fprintf(stderr
, " -p pauselen length in seconds to describe a pause between 2 requests.\n");
406 fprintf(stderr
, " -h show show command line help.\n");
407 fprintf(stderr
, "configfile is a file containing time quota definitions.\n");
410 int main(int argc
, char **argv
)
412 char request
[HELPER_INPUT_BUFFER
];
415 program_name
= argv
[0];
417 while ((opt
= getopt(argc
, argv
, "dp:l:b:h")) != -1) {
420 tq_debug_enabled
= true;
429 pauseLength
= atoi(optarg
);
438 log_info("Starting %s\n", __FILE__
);
439 setbuf(stdout
, NULL
);
443 if ( optind
+ 1 != argc
) {
447 readConfig(argv
[optind
]);
450 log_info("Waiting for requests...\n");
451 while (fgets(request
, HELPER_INPUT_BUFFER
, stdin
)) {
452 // we expect the following line syntax: %LOGIN
453 const char *user_key
= strtok(request
, " \n");
455 SEND_BH("message=\"User name missing\"");
458 processActivity(user_key
);
460 log_info("Ending %s\n", __FILE__
);