]> git.ipfire.org Git - thirdparty/squid.git/blame - src/acl/external/time_quota/ext_time_quota_acl.cc
Source Format Enforcement (#532)
[thirdparty/squid.git] / src / acl / external / time_quota / ext_time_quota_acl.cc
CommitLineData
ca02e0ec 1/*
77b1029d 2 * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
ca02e0ec
AJ
3 *
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.
7 */
8
9938b57f
TB
9/*
10 * ext_time_quota_acl: Squid external acl helper for quota on usage.
11 *
12 * Copyright (C) 2011 Dr. Tilmann Bubeck <t.bubeck@reinform.de>
13 *
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.
18 *
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.
23 *
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.
27 */
28
f7f3304a 29#include "squid.h"
079b1d0f 30#include "helper/protocol_defines.h"
9938b57f 31
074d6a40
AJ
32#include <cstdlib>
33#include <cstring>
34#include <ctime>
9938b57f
TB
35#include <sys/types.h>
36#include <sys/stat.h>
37#include <fcntl.h>
9938b57f
TB
38#if HAVE_UNISTD_H
39#include <unistd.h>
40#endif
9938b57f
TB
41#if HAVE_GETOPT_H
42#include <getopt.h>
43#endif
acd207af
AJ
44#if HAVE_TDB_H
45#include <tdb.h>
9938b57f
TB
46#endif
47
48#ifndef DEFAULT_QUOTA_DB
49#error "Please define DEFAULT_QUOTA_DB preprocessor constant."
50#endif
51
52const char *db_path = DEFAULT_QUOTA_DB;
53const char *program_name;
54
acd207af 55TDB_CONTEXT *db = nullptr;
9938b57f
TB
56
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"
62
63/** Maximum size of buffers used to read or display lines. */
64#define TQ_BUFFERSIZE 1024
65
66/** If there is more than this given number of seconds between two
f8901ea9 67 * sucessive requests, than the second request will be treated as a
9938b57f
TB
68 * new request and the time between first and seconds request will
69 * be treated as a activity pause.
70 *
71 * Otherwise the following request will be treated as belonging to the
72 * same activity and the quota will be reduced.
73 */
74static int pauseLength = 300;
75
76static FILE *logfile = stderr;
77static int tq_debug_enabled = false;
78
f8901ea9
A
79static void open_log(const char *logfilename)
80{
9938b57f
TB
81 logfile = fopen(logfilename, "a");
82 if ( logfile == NULL ) {
f8901ea9
A
83 perror(logfilename);
84 logfile = stderr;
9938b57f
TB
85 }
86}
87
88static void vlog(const char *level, const char *format, va_list args)
89{
90 time_t now = time(NULL);
91
3876fdac
A
92 fprintf(logfile, "%ld %s| %s: ", static_cast<long int>(now),
93 program_name, level);
9938b57f
TB
94 vfprintf (logfile, format, args);
95 fflush(logfile);
96}
97
98static void log_debug(const char *format, ...)
99{
100 va_list args;
101
102 if ( tq_debug_enabled ) {
f8901ea9
A
103 va_start (args, format);
104 vlog("DEBUG", format, args);
105 va_end (args);
9938b57f
TB
106 }
107}
108
109static void log_info(const char *format, ...)
110{
111 va_list args;
112
113 va_start (args, format);
114 vlog("INFO", format, args);
115 va_end (args);
116}
117
118static void log_error(const char *format, ...)
119{
120 va_list args;
121
122 va_start (args, format);
123 vlog("ERROR", format, args);
124 va_end (args);
125}
126
127static void log_fatal(const char *format, ...)
128{
129 va_list args;
130
131 va_start (args, format);
132 vlog("FATAL", format, args);
133 va_end (args);
134}
135
136static void init_db(void)
137{
138 log_info("opening time quota database \"%s\".\n", db_path);
acd207af 139 db = tdb_open(db_path, 0, TDB_CLEAR_IF_FIRST, O_CREAT | O_RDWR, 0666);
9938b57f
TB
140 if (!db) {
141 log_fatal("Failed to open time_quota db '%s'\n", db_path);
24885773 142 exit(EXIT_FAILURE);
9938b57f
TB
143 }
144}
145
146static void shutdown_db(void)
147{
acd207af 148 tdb_close(db);
9938b57f
TB
149}
150
acd207af 151static char *KeyString(int &len, const char *user_key, const char *sub_key)
9938b57f 152{
acd207af
AJ
153 static char keybuffer[TQ_BUFFERSIZE];
154 *keybuffer = 0;
155
156 len = snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
157 if (len < 0) {
158 log_error("Cannot add entry: %s-%s", user_key, sub_key);
159 len = 0;
9938b57f 160
acd207af 161 } else if (static_cast<size_t>(len) >= sizeof(keybuffer)) {
f8901ea9 162 log_error("key too long (%s,%s)\n", user_key, sub_key);
acd207af
AJ
163 len = 0;
164 }
165
166 return keybuffer;
167}
168
169static void writeTime(const char *user_key, const char *sub_key, time_t t)
170{
171 int len = 0;
172 if (/* const */ char *keybuffer = KeyString(len, user_key, sub_key)) {
173
174 TDB_DATA key, data;
175
176 key.dptr = reinterpret_cast<unsigned char *>(keybuffer);
177 key.dsize = len;
f8901ea9 178
acd207af
AJ
179 data.dptr = reinterpret_cast<unsigned char *>(&t);
180 data.dsize = sizeof(t);
181
182 tdb_store(db, key, data, TDB_REPLACE);
f8901ea9 183 log_debug("writeTime(\"%s\", %d)\n", keybuffer, t);
9938b57f
TB
184 }
185}
186
187static time_t readTime(const char *user_key, const char *sub_key)
188{
acd207af
AJ
189 int len = 0;
190 if (/* const */ char *keybuffer = KeyString(len, user_key, sub_key)) {
9938b57f 191
acd207af
AJ
192 TDB_DATA key;
193 key.dptr = reinterpret_cast<unsigned char *>(keybuffer);
194 key.dsize = len;
f8901ea9 195
acd207af
AJ
196 auto data = tdb_fetch(db, key);
197
198 time_t t = 0;
199 if (data.dsize != sizeof(t)) {
200 log_error("CORRUPTED DATABASE (%s)\n", keybuffer);
201 } else {
202 memcpy(&t, data.dptr, sizeof(t));
f8901ea9 203 }
acd207af 204
f8901ea9 205 log_debug("readTime(\"%s\")=%d\n", keybuffer, t);
acd207af 206 return t;
9938b57f
TB
207 }
208
acd207af 209 return 0;
9938b57f
TB
210}
211
212static void parseTime(const char *s, time_t *secs, time_t *start)
213{
214 double value;
215 char unit;
216 struct tm *ltime;
217 int periodLength = 3600;
218
219 *secs = 0;
220 *start = time(NULL);
221 ltime = localtime(start);
222
223 sscanf(s, " %lf %c", &value, &unit);
224 switch (unit) {
225 case 's':
f8901ea9
A
226 periodLength = 1;
227 break;
9938b57f 228 case 'm':
f8901ea9
A
229 periodLength = 60;
230 *start -= ltime->tm_sec;
231 break;
9938b57f 232 case 'h':
f8901ea9
A
233 periodLength = 3600;
234 *start -= ltime->tm_min * 60 + ltime->tm_sec;
235 break;
9938b57f 236 case 'd':
f8901ea9
A
237 periodLength = 24 * 3600;
238 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
239 break;
9938b57f 240 case 'w':
f8901ea9
A
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
245 break;
9938b57f 246 default:
f8901ea9
A
247 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit);
248 break;
9938b57f
TB
249 }
250
251 *secs = (long)(periodLength * value);
252}
253
9938b57f
TB
254/** This function parses the time quota file and stores it
255 * in memory.
256 */
f8901ea9 257static void readConfig(const char *filename)
9938b57f
TB
258{
259 char line[TQ_BUFFERSIZE]; /* the buffer for the lines read
f53969cc
SM
260 from the dict file */
261 char *cp; /* a char pointer used to parse
262 each line */
263 char *username; /* for the username */
9938b57f
TB
264 char *budget;
265 char *period;
266 FILE *FH;
267 time_t t;
268 time_t budgetSecs, periodSecs;
269 time_t start;
270
271 log_info("reading config file \"%s\".\n", filename);
272
273 FH = fopen(filename, "r");
274 if ( FH ) {
f8901ea9 275 /* the pointer to the first entry in the linked list */
03f581b0
AJ
276 unsigned int lineCount = 0;
277 while (fgets(line, sizeof(line), FH)) {
278 ++lineCount;
f8901ea9
A
279 if (line[0] == '#') {
280 continue;
281 }
282 if ((cp = strchr (line, '\n')) != NULL) {
283 /* chop \n characters */
284 *cp = '\0';
285 }
03f581b0
AJ
286 log_debug("read config line %u: \"%s\".\n", lineCount, line);
287 if ((username = strtok(line, "\t ")) != NULL) {
f8901ea9
A
288
289 /* get the time budget */
03f581b0
AJ
290 if ((budget = strtok(NULL, "/")) == NULL) {
291 fprintf(stderr, "ERROR: missing 'budget' field on line %u of '%s'.\n", lineCount, filename);
292 continue;
293 }
294 if ((period = strtok(NULL, "/")) == NULL) {
295 fprintf(stderr, "ERROR: missing 'period' field on line %u of '%s'.\n", lineCount, filename);
296 continue;
297 }
f8901ea9
A
298
299 parseTime(budget, &budgetSecs, &start);
300 parseTime(period, &periodSecs, &start);
301
302 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
303 username, budgetSecs, periodSecs, start);
304
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);
310 }
311 }
312 fclose(FH);
9938b57f 313 } else {
f8901ea9 314 perror(filename);
9938b57f
TB
315 }
316}
317
318static void processActivity(const char *user_key)
319{
f8901ea9
A
320 time_t now = time(NULL);
321 time_t lastActivity;
322 time_t activityLength;
323 time_t periodStart;
324 time_t periodLength;
325 time_t userPeriodLength;
326 time_t timeBudgetCurrent;
327 time_t timeBudgetConfigured;
328 char message[TQ_BUFFERSIZE];
329
330 log_debug("processActivity(\"%s\")\n", user_key);
331
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.
336 periodStart = now;
337 writeTime(user_key, KEY_PERIOD_START, periodStart);
338 }
339
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);
346 } else {
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;
352 }
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);
358 } else {
359 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetConfigured);
360 }
361 }
362 }
363
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);
369 } else {
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);
375 } else {
376 // This is real usage.
377 writeTime(user_key, KEY_LAST_ACTIVITY, now);
378
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);
384 }
385 }
386
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);
391 SEND_OK(message);
392 } else {
393 log_debug("ERR %s\n", message);
394 SEND_ERR("Time budget exceeded.");
395 }
9938b57f
TB
396}
397
398static void usage(void)
399{
400 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
401
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");
410}
411
412int main(int argc, char **argv)
413{
414 char request[HELPER_INPUT_BUFFER];
415 int opt;
416
417 program_name = argv[0];
418
419 while ((opt = getopt(argc, argv, "dp:l:b:h")) != -1) {
420 switch (opt) {
421 case 'd':
f8901ea9 422 tq_debug_enabled = true;
9938b57f
TB
423 break;
424 case 'l':
f8901ea9 425 open_log(optarg);
9938b57f
TB
426 break;
427 case 'b':
428 db_path = optarg;
429 break;
430 case 'p':
431 pauseLength = atoi(optarg);
432 break;
433 case 'h':
434 usage();
24885773 435 exit(EXIT_SUCCESS);
9938b57f
TB
436 break;
437 }
438 }
439
440 log_info("Starting %s\n", __FILE__);
441 setbuf(stdout, NULL);
442
443 init_db();
444
445 if ( optind + 1 != argc ) {
f8901ea9 446 usage();
24885773 447 exit(EXIT_FAILURE);
9938b57f 448 } else {
f8901ea9 449 readConfig(argv[optind]);
9938b57f
TB
450 }
451
452 log_info("Waiting for requests...\n");
453 while (fgets(request, HELPER_INPUT_BUFFER, stdin)) {
03f581b0
AJ
454 // we expect the following line syntax: %LOGIN
455 const char *user_key = strtok(request, " \n");
456 if (!user_key) {
194ccc9c 457 SEND_BH(HLP_MSG("User name missing"));
03f581b0
AJ
458 continue;
459 }
f8901ea9 460 processActivity(user_key);
9938b57f
TB
461 }
462 log_info("Ending %s\n", __FILE__);
463 shutdown_db();
24885773 464 return EXIT_SUCCESS;
9938b57f 465}
f53969cc 466