2 * Copyright (C) 1996-2023 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 "helper/protocol_defines.h"
35 #include <sys/types.h>
48 #ifndef DEFAULT_QUOTA_DB
49 #error "Please define DEFAULT_QUOTA_DB preprocessor constant."
52 const char *db_path
= DEFAULT_QUOTA_DB
;
53 const char *program_name
;
55 TDB_CONTEXT
*db
= nullptr;
57 #define KEY_LAST_ACTIVITY "last-activity"
58 #define KEY_PERIOD_START "period-start"
59 #define KEY_PERIOD_LENGTH_CONFIGURED "period-length-configured"
60 #define KEY_TIME_BUDGET_LEFT "time-budget-left"
61 #define KEY_TIME_BUDGET_CONFIGURED "time-budget-configured"
63 /** Maximum size of buffers used to read or display lines. */
64 #define TQ_BUFFERSIZE 1024
66 /** If there is more than this given number of seconds between two
67 * successive requests, than the second request will be treated as a
68 * new request and the time between first and seconds request will
69 * be treated as a activity pause.
71 * Otherwise the following request will be treated as belonging to the
72 * same activity and the quota will be reduced.
74 static int pauseLength
= 300;
76 static FILE *logfile
= stderr
;
77 static int tq_debug_enabled
= false;
79 static void open_log(const char *logfilename
)
81 logfile
= fopen(logfilename
, "a");
82 if ( logfile
== NULL
) {
88 static void vlog(const char *level
, const char *format
, va_list args
)
90 time_t now
= time(NULL
);
92 fprintf(logfile
, "%ld %s| %s: ", static_cast<long int>(now
),
94 vfprintf (logfile
, format
, args
);
98 static void log_debug(const char *format
, ...)
102 if ( tq_debug_enabled
) {
103 va_start (args
, format
);
104 vlog("DEBUG", format
, args
);
109 static void log_info(const char *format
, ...)
113 va_start (args
, format
);
114 vlog("INFO", format
, args
);
118 static void log_error(const char *format
, ...)
122 va_start (args
, format
);
123 vlog("ERROR", format
, args
);
127 static void log_fatal(const char *format
, ...)
131 va_start (args
, format
);
132 vlog("FATAL", format
, args
);
136 static void init_db(void)
138 log_info("opening time quota database \"%s\".\n", db_path
);
139 db
= tdb_open(db_path
, 0, TDB_CLEAR_IF_FIRST
, O_CREAT
| O_RDWR
, 0666);
141 log_fatal("Failed to open time_quota db '%s'\n", db_path
);
146 static void shutdown_db(void)
151 static char *KeyString(int &len
, const char *user_key
, const char *sub_key
)
153 static char keybuffer
[TQ_BUFFERSIZE
];
156 len
= snprintf(keybuffer
, sizeof(keybuffer
), "%s-%s", user_key
, sub_key
);
158 log_error("Cannot add entry: %s-%s", user_key
, sub_key
);
161 } else if (static_cast<size_t>(len
) >= sizeof(keybuffer
)) {
162 log_error("key too long (%s,%s)\n", user_key
, sub_key
);
169 static void writeTime(const char *user_key
, const char *sub_key
, time_t t
)
172 if (/* const */ char *keybuffer
= KeyString(len
, user_key
, sub_key
)) {
176 key
.dptr
= reinterpret_cast<unsigned char *>(keybuffer
);
179 data
.dptr
= reinterpret_cast<unsigned char *>(&t
);
180 data
.dsize
= sizeof(t
);
182 tdb_store(db
, key
, data
, TDB_REPLACE
);
183 log_debug("writeTime(\"%s\", %d)\n", keybuffer
, t
);
187 static time_t readTime(const char *user_key
, const char *sub_key
)
190 if (/* const */ char *keybuffer
= KeyString(len
, user_key
, sub_key
)) {
193 key
.dptr
= reinterpret_cast<unsigned char *>(keybuffer
);
196 auto data
= tdb_fetch(db
, key
);
199 if (data
.dsize
!= sizeof(t
)) {
200 log_error("CORRUPTED DATABASE (%s)\n", keybuffer
);
202 memcpy(&t
, data
.dptr
, sizeof(t
));
205 log_debug("readTime(\"%s\")=%d\n", keybuffer
, t
);
212 static void parseTime(const char *s
, time_t *secs
, time_t *start
)
217 int periodLength
= 3600;
221 ltime
= localtime(start
);
223 sscanf(s
, " %lf %c", &value
, &unit
);
230 *start
-= ltime
->tm_sec
;
234 *start
-= ltime
->tm_min
* 60 + ltime
->tm_sec
;
237 periodLength
= 24 * 3600;
238 *start
-= ltime
->tm_hour
* 3600 + ltime
->tm_min
* 60 + ltime
->tm_sec
;
241 periodLength
= 7 * 24 * 3600;
242 *start
-= ltime
->tm_hour
* 3600 + ltime
->tm_min
* 60 + ltime
->tm_sec
;
243 *start
-= ltime
->tm_wday
* 24 * 3600;
244 *start
+= 24 * 3600; // in europe, the week starts monday
247 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit
);
251 *secs
= (long)(periodLength
* value
);
254 /** This function parses the time quota file and stores it
257 static void readConfig(const char *filename
)
259 char line
[TQ_BUFFERSIZE
]; /* the buffer for the lines read
260 from the dict file */
261 char *cp
; /* a char pointer used to parse
263 char *username
; /* for the username */
268 time_t budgetSecs
, periodSecs
;
271 log_info("reading config file \"%s\".\n", filename
);
273 FH
= fopen(filename
, "r");
275 /* the pointer to the first entry in the linked list */
276 unsigned int lineCount
= 0;
277 while (fgets(line
, sizeof(line
), FH
)) {
279 if (line
[0] == '#') {
282 if ((cp
= strchr (line
, '\n')) != NULL
) {
283 /* chop \n characters */
286 log_debug("read config line %u: \"%s\".\n", lineCount
, line
);
287 if ((username
= strtok(line
, "\t ")) != NULL
) {
289 /* get the time budget */
290 if ((budget
= strtok(nullptr, "/")) == NULL
) {
291 fprintf(stderr
, "ERROR: missing 'budget' field on line %u of '%s'.\n", lineCount
, filename
);
294 if ((period
= strtok(nullptr, "/")) == NULL
) {
295 fprintf(stderr
, "ERROR: missing 'period' field on line %u of '%s'.\n", lineCount
, filename
);
299 parseTime(budget
, &budgetSecs
, &start
);
300 parseTime(period
, &periodSecs
, &start
);
302 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
303 username
, budgetSecs
, periodSecs
, start
);
305 writeTime(username
, KEY_PERIOD_START
, start
);
306 writeTime(username
, KEY_PERIOD_LENGTH_CONFIGURED
, periodSecs
);
307 writeTime(username
, KEY_TIME_BUDGET_CONFIGURED
, budgetSecs
);
308 t
= readTime(username
, KEY_TIME_BUDGET_CONFIGURED
);
309 writeTime(username
, KEY_TIME_BUDGET_LEFT
, t
);
318 static void processActivity(const char *user_key
)
320 time_t now
= time(NULL
);
322 time_t activityLength
;
325 time_t userPeriodLength
;
326 time_t timeBudgetCurrent
;
327 time_t timeBudgetConfigured
;
328 char message
[TQ_BUFFERSIZE
];
330 log_debug("processActivity(\"%s\")\n", user_key
);
332 // [1] Reset period if over
333 periodStart
= readTime(user_key
, KEY_PERIOD_START
);
334 if ( periodStart
== 0 ) {
335 // This is the first period ever.
337 writeTime(user_key
, KEY_PERIOD_START
, periodStart
);
340 periodLength
= now
- periodStart
;
341 userPeriodLength
= readTime(user_key
, KEY_PERIOD_LENGTH_CONFIGURED
);
342 if ( userPeriodLength
== 0 ) {
343 // This user is not configured. Allow anything.
344 log_debug("No period length found for user \"%s\". Quota for this user disabled.\n", user_key
);
345 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, pauseLength
);
347 if ( periodLength
>= userPeriodLength
) {
348 // a new period has started.
349 log_debug("New time period started for user \"%s\".\n", user_key
);
350 while ( periodStart
< now
) {
351 periodStart
+= periodLength
;
353 writeTime(user_key
, KEY_PERIOD_START
, periodStart
);
354 timeBudgetConfigured
= readTime(user_key
, KEY_TIME_BUDGET_CONFIGURED
);
355 if ( timeBudgetConfigured
== 0 ) {
356 log_debug("No time budget configured for user \"%s\". Quota for this user disabled.\n", user_key
);
357 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, pauseLength
);
359 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, timeBudgetConfigured
);
364 // [2] Decrease time budget iff activity
365 lastActivity
= readTime(user_key
, KEY_LAST_ACTIVITY
);
366 if ( lastActivity
== 0 ) {
367 // This is the first request ever
368 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
370 activityLength
= now
- lastActivity
;
371 if ( activityLength
>= pauseLength
) {
372 // This is an activity pause.
373 log_debug("Activity pause detected for user \"%s\".\n", user_key
);
374 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
376 // This is real usage.
377 writeTime(user_key
, KEY_LAST_ACTIVITY
, now
);
379 log_debug("Time budget reduced by %ld for user \"%s\".\n",
380 activityLength
, user_key
);
381 timeBudgetCurrent
= readTime(user_key
, KEY_TIME_BUDGET_LEFT
);
382 timeBudgetCurrent
-= activityLength
;
383 writeTime(user_key
, KEY_TIME_BUDGET_LEFT
, timeBudgetCurrent
);
387 timeBudgetCurrent
= readTime(user_key
, KEY_TIME_BUDGET_LEFT
);
388 snprintf(message
, TQ_BUFFERSIZE
, "message=\"Remaining quota for '%s' is %d seconds.\"", user_key
, (int)timeBudgetCurrent
);
389 if ( timeBudgetCurrent
> 0 ) {
390 log_debug("OK %s.\n", message
);
393 log_debug("ERR %s\n", message
);
394 SEND_ERR("Time budget exceeded.");
398 static void usage(void)
400 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
402 fprintf(stderr
, "Usage: %s [-d] [-l logfile] [-b dbpath] [-p pauselen] [-h] configfile\n", program_name
);
403 fprintf(stderr
, " -d enable debugging output to logfile\n");
404 fprintf(stderr
, " -l logfile log messages to logfile\n");
405 fprintf(stderr
, " -b dbpath Path where persistent session database will be kept\n");
406 fprintf(stderr
, " If option is not used, then " DEFAULT_QUOTA_DB
" will be used.\n");
407 fprintf(stderr
, " -p pauselen length in seconds to describe a pause between 2 requests.\n");
408 fprintf(stderr
, " -h show show command line help.\n");
409 fprintf(stderr
, "configfile is a file containing time quota definitions.\n");
412 int main(int argc
, char **argv
)
414 char request
[HELPER_INPUT_BUFFER
];
417 program_name
= argv
[0];
419 while ((opt
= getopt(argc
, argv
, "dp:l:b:h")) != -1) {
422 tq_debug_enabled
= true;
431 pauseLength
= atoi(optarg
);
440 log_info("Starting %s\n", __FILE__
);
441 setbuf(stdout
, nullptr);
445 if ( optind
+ 1 != argc
) {
449 readConfig(argv
[optind
]);
452 log_info("Waiting for requests...\n");
453 while (fgets(request
, HELPER_INPUT_BUFFER
, stdin
)) {
454 // we expect the following line syntax: %LOGIN
455 const char *user_key
= strtok(request
, " \n");
457 SEND_BH(HLP_MSG("User name missing"));
460 processActivity(user_key
);
462 log_info("Ending %s\n", __FILE__
);