]> git.ipfire.org Git - thirdparty/squid.git/blob - src/acl/external/time_quota/ext_time_quota_acl.cc
Docs: Copyright updates for 2018 (#114)
[thirdparty/squid.git] / src / acl / external / time_quota / ext_time_quota_acl.cc
1 /*
2 * Copyright (C) 1996-2018 The Squid Software Foundation and contributors
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
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
29 #include "squid.h"
30 #include "helper/protocol_defines.h"
31
32 #include <cstdarg>
33 #include <cstdlib>
34 #include <cstring>
35 #include <ctime>
36 #include <sys/types.h>
37 #include <sys/stat.h>
38 #include <fcntl.h>
39 #if HAVE_UNISTD_H
40 #include <unistd.h>
41 #endif
42 #if HAVE_GETOPT_H
43 #include <getopt.h>
44 #endif
45
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.
49 */
50 #ifndef __BIT_TYPES_DEFINED__
51 #define __BIT_TYPES_DEFINED__
52 #endif
53
54 #if HAVE_DB_185_H
55 #include <db_185.h>
56 #elif HAVE_DB_H
57 #include <db.h>
58 #endif
59
60 #ifndef DEFAULT_QUOTA_DB
61 #error "Please define DEFAULT_QUOTA_DB preprocessor constant."
62 #endif
63
64 const char *db_path = DEFAULT_QUOTA_DB;
65 const char *program_name;
66
67 DB *db = NULL;
68
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"
74
75 /** Maximum size of buffers used to read or display lines. */
76 #define TQ_BUFFERSIZE 1024
77
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.
82 *
83 * Otherwise the following request will be treated as belonging to the
84 * same activity and the quota will be reduced.
85 */
86 static int pauseLength = 300;
87
88 static FILE *logfile = stderr;
89 static int tq_debug_enabled = false;
90
91 static void open_log(const char *logfilename)
92 {
93 logfile = fopen(logfilename, "a");
94 if ( logfile == NULL ) {
95 perror(logfilename);
96 logfile = stderr;
97 }
98 }
99
100 static void vlog(const char *level, const char *format, va_list args)
101 {
102 time_t now = time(NULL);
103
104 fprintf(logfile, "%ld %s| %s: ", static_cast<long int>(now),
105 program_name, level);
106 vfprintf (logfile, format, args);
107 fflush(logfile);
108 }
109
110 static void log_debug(const char *format, ...)
111 {
112 va_list args;
113
114 if ( tq_debug_enabled ) {
115 va_start (args, format);
116 vlog("DEBUG", format, args);
117 va_end (args);
118 }
119 }
120
121 static void log_info(const char *format, ...)
122 {
123 va_list args;
124
125 va_start (args, format);
126 vlog("INFO", format, args);
127 va_end (args);
128 }
129
130 static void log_error(const char *format, ...)
131 {
132 va_list args;
133
134 va_start (args, format);
135 vlog("ERROR", format, args);
136 va_end (args);
137 }
138
139 static void log_fatal(const char *format, ...)
140 {
141 va_list args;
142
143 va_start (args, format);
144 vlog("FATAL", format, args);
145 va_end (args);
146 }
147
148 static void init_db(void)
149 {
150 log_info("opening time quota database \"%s\".\n", db_path);
151 db = dbopen(db_path, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL);
152 if (!db) {
153 log_fatal("Failed to open time_quota db '%s'\n", db_path);
154 exit(EXIT_FAILURE);
155 }
156 }
157
158 static void shutdown_db(void)
159 {
160 db->close(db);
161 }
162
163 static void writeTime(const char *user_key, const char *sub_key, time_t t)
164 {
165 char keybuffer[TQ_BUFFERSIZE];
166 DBT key, data;
167
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);
170 } else {
171 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
172
173 key.data = (void *)keybuffer;
174 key.size = strlen(keybuffer);
175 data.data = &t;
176 data.size = sizeof(t);
177 db->put(db, &key, &data, 0);
178 log_debug("writeTime(\"%s\", %d)\n", keybuffer, t);
179 }
180 }
181
182 static time_t readTime(const char *user_key, const char *sub_key)
183 {
184 char keybuffer[TQ_BUFFERSIZE];
185 DBT key, data;
186 time_t t = 0;
187
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);
190 } else {
191 snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
192
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);
198 } else {
199 memcpy(&t, data.data, sizeof(t));
200 }
201 }
202 log_debug("readTime(\"%s\")=%d\n", keybuffer, t);
203 }
204
205 return t;
206 }
207
208 static void parseTime(const char *s, time_t *secs, time_t *start)
209 {
210 double value;
211 char unit;
212 struct tm *ltime;
213 int periodLength = 3600;
214
215 *secs = 0;
216 *start = time(NULL);
217 ltime = localtime(start);
218
219 sscanf(s, " %lf %c", &value, &unit);
220 switch (unit) {
221 case 's':
222 periodLength = 1;
223 break;
224 case 'm':
225 periodLength = 60;
226 *start -= ltime->tm_sec;
227 break;
228 case 'h':
229 periodLength = 3600;
230 *start -= ltime->tm_min * 60 + ltime->tm_sec;
231 break;
232 case 'd':
233 periodLength = 24 * 3600;
234 *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
235 break;
236 case 'w':
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
241 break;
242 default:
243 log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit);
244 break;
245 }
246
247 *secs = (long)(periodLength * value);
248 }
249
250 /** This function parses the time quota file and stores it
251 * in memory.
252 */
253 static void readConfig(const char *filename)
254 {
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
258 each line */
259 char *username; /* for the username */
260 char *budget;
261 char *period;
262 FILE *FH;
263 time_t t;
264 time_t budgetSecs, periodSecs;
265 time_t start;
266
267 log_info("reading config file \"%s\".\n", filename);
268
269 FH = fopen(filename, "r");
270 if ( FH ) {
271 /* the pointer to the first entry in the linked list */
272 unsigned int lineCount = 0;
273 while (fgets(line, sizeof(line), FH)) {
274 ++lineCount;
275 if (line[0] == '#') {
276 continue;
277 }
278 if ((cp = strchr (line, '\n')) != NULL) {
279 /* chop \n characters */
280 *cp = '\0';
281 }
282 log_debug("read config line %u: \"%s\".\n", lineCount, line);
283 if ((username = strtok(line, "\t ")) != NULL) {
284
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);
288 continue;
289 }
290 if ((period = strtok(NULL, "/")) == NULL) {
291 fprintf(stderr, "ERROR: missing 'period' field on line %u of '%s'.\n", lineCount, filename);
292 continue;
293 }
294
295 parseTime(budget, &budgetSecs, &start);
296 parseTime(period, &periodSecs, &start);
297
298 log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
299 username, budgetSecs, periodSecs, start);
300
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);
306 }
307 }
308 fclose(FH);
309 } else {
310 perror(filename);
311 }
312 }
313
314 static void processActivity(const char *user_key)
315 {
316 time_t now = time(NULL);
317 time_t lastActivity;
318 time_t activityLength;
319 time_t periodStart;
320 time_t periodLength;
321 time_t userPeriodLength;
322 time_t timeBudgetCurrent;
323 time_t timeBudgetConfigured;
324 char message[TQ_BUFFERSIZE];
325
326 log_debug("processActivity(\"%s\")\n", user_key);
327
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.
332 periodStart = now;
333 writeTime(user_key, KEY_PERIOD_START, periodStart);
334 }
335
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);
342 } else {
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;
348 }
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);
354 } else {
355 writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetConfigured);
356 }
357 }
358 }
359
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);
365 } else {
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);
371 } else {
372 // This is real usage.
373 writeTime(user_key, KEY_LAST_ACTIVITY, now);
374
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);
380 }
381 }
382
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);
387 SEND_OK(message);
388 } else {
389 log_debug("ERR %s\n", message);
390 SEND_ERR("Time budget exceeded.");
391 }
392
393 db->sync(db, 0);
394 }
395
396 static void usage(void)
397 {
398 log_error("Wrong usage. Please reconfigure in squid.conf.\n");
399
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");
408 }
409
410 int main(int argc, char **argv)
411 {
412 char request[HELPER_INPUT_BUFFER];
413 int opt;
414
415 program_name = argv[0];
416
417 while ((opt = getopt(argc, argv, "dp:l:b:h")) != -1) {
418 switch (opt) {
419 case 'd':
420 tq_debug_enabled = true;
421 break;
422 case 'l':
423 open_log(optarg);
424 break;
425 case 'b':
426 db_path = optarg;
427 break;
428 case 'p':
429 pauseLength = atoi(optarg);
430 break;
431 case 'h':
432 usage();
433 exit(EXIT_SUCCESS);
434 break;
435 }
436 }
437
438 log_info("Starting %s\n", __FILE__);
439 setbuf(stdout, NULL);
440
441 init_db();
442
443 if ( optind + 1 != argc ) {
444 usage();
445 exit(EXIT_FAILURE);
446 } else {
447 readConfig(argv[optind]);
448 }
449
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");
454 if (!user_key) {
455 SEND_BH(HLP_MSG("User name missing"));
456 continue;
457 }
458 processActivity(user_key);
459 }
460 log_info("Ending %s\n", __FILE__);
461 shutdown_db();
462 return EXIT_SUCCESS;
463 }
464