ext_time_quota_acl.cc
Go to the documentation of this file.
1 /*
2  * Copyright (C) 1996-2023 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 /* DEBUG: section 82 External ACL Helpers */
30 
31 #include "squid.h"
32 #include "debug/Stream.h"
34 #include "sbuf/Stream.h"
35 
36 #include <ctime>
37 #if HAVE_GETOPT_H
38 #include <getopt.h>
39 #endif
40 #if HAVE_TDB_H
41 #include <tdb.h>
42 #endif
43 
44 #ifndef DEFAULT_QUOTA_DB
45 #error "Please define DEFAULT_QUOTA_DB preprocessor constant."
46 #endif
47 
48 static const auto MY_DEBUG_SECTION = 82;
49 const char *db_path = DEFAULT_QUOTA_DB;
50 const char *program_name;
51 
52 TDB_CONTEXT *db = nullptr;
53 
54 static const auto KeyLastActivity = "last-activity";
55 static const auto KeyPeriodStart = "period-start";
56 static const auto KeyPeriodLengthConfigured = "period-length-configured";
57 static const auto KeyTimeBudgetLeft = "time-budget-left";
58 static const auto KeyTimeBudgetConfigured = "time-budget-configured";
59 
61 static const size_t TQ_BUFFERSIZE = 1024;
62 
71 static int pauseLength = 300;
72 
73 static void init_db(void)
74 {
75  debugs(MY_DEBUG_SECTION, 2, "opening time quota database \"" << db_path << "\".");
76 
77  db = tdb_open(db_path, 0, TDB_CLEAR_IF_FIRST, O_CREAT | O_RDWR, 0666);
78  if (!db) {
79  debugs(MY_DEBUG_SECTION, DBG_CRITICAL, "FATAL: Failed to open time_quota db '" << db_path << '\'');
80  exit(EXIT_FAILURE);
81  }
82  // count the number of entries in the database, only used for debugging
83  debugs(MY_DEBUG_SECTION, 2, "Database contains " << tdb_traverse(db, nullptr, nullptr) << " entries");
84 }
85 
86 static void shutdown_db(void)
87 {
88  tdb_close(db);
89 }
90 
91 static SBuf KeyString(const char *user_key, const char *sub_key)
92 {
93  return ToSBuf(user_key, "-", sub_key);
94 }
95 
96 static void writeTime(const char *user_key, const char *sub_key, time_t t)
97 {
98  auto ks = KeyString(user_key, sub_key);
99  const TDB_DATA key {
100  reinterpret_cast<unsigned char *>(const_cast<char *>(ks.rawContent())),
101  ks.length()
102  };
103  const TDB_DATA data {
104  reinterpret_cast<unsigned char *>(&t),
105  sizeof(t)
106  };
107 
108  tdb_store(db, key, data, TDB_REPLACE);
109  debugs(MY_DEBUG_SECTION, 3, "writeTime(\"" << ks << "\", " << t << ')');
110 }
111 
112 static time_t readTime(const char *user_key, const char *sub_key)
113 {
114  auto ks = KeyString(user_key, sub_key);
115  const TDB_DATA key {
116  reinterpret_cast<unsigned char *>(const_cast<char *>(ks.rawContent())),
117  ks.length()
118  };
119  auto data = tdb_fetch(db, key);
120 
121  if (!data.dptr) {
122  debugs(MY_DEBUG_SECTION, 3, "no data found for key \"" << ks << "\".");
123  return 0;
124  }
125 
126  time_t t = 0;
127  if (data.dsize == sizeof(t)) {
128  memcpy(&t, data.dptr, sizeof(t));
129  } else {
130  debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: Incompatible or corrupted database. " <<
131  "key: '" << ks <<
132  "', expected time value size: " << sizeof(t) <<
133  ", actual time value size: " << data.dsize);
134  }
135 
136  debugs(MY_DEBUG_SECTION, 3, "readTime(\"" << ks << "\")=" << t);
137  return t;
138 }
139 
140 static void parseTime(const char *s, time_t *secs, time_t *start)
141 {
142  double value;
143  char unit;
144  struct tm *ltime;
145  int periodLength = 3600;
146 
147  *secs = 0;
148  *start = time(NULL);
149  ltime = localtime(start);
150 
151  sscanf(s, " %lf %c", &value, &unit);
152  switch (unit) {
153  case 's':
154  periodLength = 1;
155  break;
156  case 'm':
157  periodLength = 60;
158  *start -= ltime->tm_sec;
159  break;
160  case 'h':
161  periodLength = 3600;
162  *start -= ltime->tm_min * 60 + ltime->tm_sec;
163  break;
164  case 'd':
165  periodLength = 24 * 3600;
166  *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
167  break;
168  case 'w':
169  periodLength = 7 * 24 * 3600;
170  *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
171  *start -= ltime->tm_wday * 24 * 3600;
172  *start += 24 * 3600; // in europe, the week starts monday
173  break;
174  default:
175  debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: Wrong time unit \"" << unit << "\". Only \"m\", \"h\", \"d\", or \"w\" allowed");
176  break;
177  }
178 
179  *secs = (long)(periodLength * value);
180 }
181 
185 static void readConfig(const char *filename)
186 {
187  char line[TQ_BUFFERSIZE]; /* the buffer for the lines read
188  from the dict file */
189  char *cp; /* a char pointer used to parse
190  each line */
191  char *username; /* for the username */
192  char *budget;
193  char *period;
194  FILE *FH;
195  time_t t;
196  time_t budgetSecs, periodSecs;
197  time_t start;
198 
199  debugs(MY_DEBUG_SECTION, 2, "reading config file \"" << filename << "\".");
200 
201  FH = fopen(filename, "r");
202  if ( FH ) {
203  /* the pointer to the first entry in the linked list */
204  unsigned int lineCount = 0;
205  while (fgets(line, sizeof(line), FH)) {
206  ++lineCount;
207  if (line[0] == '#') {
208  continue;
209  }
210  if ((cp = strchr (line, '\n')) != NULL) {
211  /* chop \n characters */
212  *cp = '\0';
213  }
214  debugs(MY_DEBUG_SECTION, 3, "read config line " << lineCount << ": \"" << line << '\"');
215  if ((username = strtok(line, "\t ")) != NULL) {
216 
217  /* get the time budget */
218  if ((budget = strtok(nullptr, "/")) == NULL) {
219  debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: missing 'budget' field on line " << lineCount << " of '" << filename << '\'');
220  continue;
221  }
222  if ((period = strtok(nullptr, "/")) == NULL) {
223  debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: missing 'period' field on line " << lineCount << " of '" << filename << '\'');
224  continue;
225  }
226 
227  parseTime(budget, &budgetSecs, &start);
228  parseTime(period, &periodSecs, &start);
229 
230  debugs(MY_DEBUG_SECTION, 3, "read time quota for user \"" << username << "\": " <<
231  budgetSecs << "s / " << periodSecs << "s starting " << start);
232 
233  writeTime(username, KeyPeriodStart, start);
234  writeTime(username, KeyPeriodLengthConfigured, periodSecs);
235  writeTime(username, KeyTimeBudgetConfigured, budgetSecs);
236  t = readTime(username, KeyTimeBudgetConfigured);
237  writeTime(username, KeyTimeBudgetLeft, t);
238  }
239  }
240  fclose(FH);
241  } else {
242  perror(filename);
243  }
244 }
245 
246 static void processActivity(const char *user_key)
247 {
248  time_t now = time(NULL);
249  time_t lastActivity;
250  time_t activityLength;
251  time_t periodStart;
252  time_t periodLength;
253  time_t userPeriodLength;
254  time_t timeBudgetCurrent;
255  time_t timeBudgetConfigured;
256 
257  debugs(MY_DEBUG_SECTION, 3, "processActivity(\"" << user_key << "\")");
258 
259  // [1] Reset period if over
260  periodStart = readTime(user_key, KeyPeriodStart);
261  if ( periodStart == 0 ) {
262  // This is the first period ever.
263  periodStart = now;
264  writeTime(user_key, KeyPeriodStart, periodStart);
265  }
266 
267  periodLength = now - periodStart;
268  userPeriodLength = readTime(user_key, KeyPeriodLengthConfigured);
269  if ( userPeriodLength == 0 ) {
270  // This user is not configured. Allow anything.
271  debugs(MY_DEBUG_SECTION, 3, "disabling user quota for user '" <<
272  user_key << "': no period length found");
274  } else {
275  if ( periodLength >= userPeriodLength ) {
276  // a new period has started.
277  debugs(MY_DEBUG_SECTION, 3, "New time period started for user \"" << user_key << '\"');
278  while ( periodStart < now ) {
279  periodStart += periodLength;
280  }
281  writeTime(user_key, KeyPeriodStart, periodStart);
282  timeBudgetConfigured = readTime(user_key, KeyTimeBudgetConfigured);
283  if ( timeBudgetConfigured == 0 ) {
284  debugs(MY_DEBUG_SECTION, 3, "No time budget configured for user \"" << user_key <<
285  "\". Quota for this user disabled.");
287  } else {
288  writeTime(user_key, KeyTimeBudgetLeft, timeBudgetConfigured);
289  }
290  }
291  }
292 
293  // [2] Decrease time budget iff activity
294  lastActivity = readTime(user_key, KeyLastActivity);
295  if ( lastActivity == 0 ) {
296  // This is the first request ever
297  writeTime(user_key, KeyLastActivity, now);
298  } else {
299  activityLength = now - lastActivity;
300  if ( activityLength >= pauseLength ) {
301  // This is an activity pause.
302  debugs(MY_DEBUG_SECTION, 3, "Activity pause detected for user \"" << user_key << "\".");
303  writeTime(user_key, KeyLastActivity, now);
304  } else {
305  // This is real usage.
306  writeTime(user_key, KeyLastActivity, now);
307 
308  debugs(MY_DEBUG_SECTION, 3, "Time budget reduced by " << activityLength <<
309  " for user \"" << user_key << "\".");
310  timeBudgetCurrent = readTime(user_key, KeyTimeBudgetLeft);
311  timeBudgetCurrent -= activityLength;
312  writeTime(user_key, KeyTimeBudgetLeft, timeBudgetCurrent);
313  }
314  }
315 
316  timeBudgetCurrent = readTime(user_key, KeyTimeBudgetLeft);
317 
318  const auto message = ToSBuf(HLP_MSG("Remaining quota for '"), user_key, "' is ", timeBudgetCurrent, " seconds.");
319  if ( timeBudgetCurrent > 0 ) {
320  SEND_OK(message);
321  } else {
322  SEND_ERR("Time budget exceeded.");
323  }
324 }
325 
326 static void usage(void)
327 {
328  debugs(MY_DEBUG_SECTION, DBG_CRITICAL, "Wrong usage. Please reconfigure in squid.conf.");
329 
330  std::cerr <<
331  "Usage: " << program_name << " [-d level] [-b dbpath] [-p pauselen] [-h] configfile\n" <<
332  " -d level set section " << MY_DEBUG_SECTION << " debugging to the specified level,\n"
333  " overwriting Squid's debug_options (default: 1)\n"
334  " -b dbpath Path where persistent session database will be kept\n" <<
335  " If option is not used, then " << DEFAULT_QUOTA_DB << " will be used.\n" <<
336  " -p pauselen length in seconds to describe a pause between 2 requests.\n" <<
337  " -h show show command line help.\n" <<
338  "configfile is a file containing time quota definitions.\n";
339 }
340 
341 int main(int argc, char **argv)
342 {
343  char request[HELPER_INPUT_BUFFER];
344  int opt;
345 
346  program_name = argv[0];
347  Debug::NameThisHelper("ext_time_quota_acl");
348 
349  while ((opt = getopt(argc, argv, "d:p:b:h")) != -1) {
350  switch (opt) {
351  case 'd':
353  break;
354  case 'b':
355  db_path = optarg;
356  break;
357  case 'p':
358  pauseLength = atoi(optarg);
359  break;
360  case 'h':
361  usage();
362  exit(EXIT_SUCCESS);
363  break;
364  default:
365  // getopt() emits error message to stderr
366  usage();
367  exit(EXIT_FAILURE);
368  break;
369  }
370  }
371 
373  setbuf(stdout, nullptr);
374 
375  init_db();
376 
377  if ( optind + 1 != argc ) {
378  usage();
379  exit(EXIT_FAILURE);
380  } else {
381  readConfig(argv[optind]);
382  }
383 
384  debugs(MY_DEBUG_SECTION, 2, "Waiting for requests...");
385  while (fgets(request, HELPER_INPUT_BUFFER, stdin)) {
386  // we expect the following line syntax: %LOGIN
387  const char *user_key = strtok(request, " \n");
388  if (!user_key) {
389  SEND_BH(HLP_MSG("User name missing"));
390  continue;
391  }
392  processActivity(user_key);
393  }
395  shutdown_db();
396  return EXIT_SUCCESS;
397 }
398 
const char * program_name
#define DBG_CRITICAL
Definition: Stream.h:37
const char * db_path
int main(int argc, char **argv)
Definition: SBuf.h:93
static void readConfig(const char *filename)
static const auto KeyLastActivity
TDB_CONTEXT * db
static void usage(void)
char * optarg
Definition: getopt.c:51
static void processActivity(const char *user_key)
int getopt(int nargc, char *const *nargv, const char *ostr)
Definition: getopt.c:62
static int pauseLength
static SBuf KeyString(const char *user_key, const char *sub_key)
static const auto KeyTimeBudgetLeft
#define NULL
Definition: types.h:145
#define SEND_ERR(x)
static void NameThisHelper(const char *name)
Definition: debug.cc:384
static const auto MY_DEBUG_SECTION
static const auto KeyTimeBudgetConfigured
static void parseTime(const char *s, time_t *secs, time_t *start)
#define SEND_BH(x)
static void parseOptions(char const *)
Definition: debug.cc:1095
static time_t readTime(const char *user_key, const char *sub_key)
static const size_t TQ_BUFFERSIZE
#define HELPER_INPUT_BUFFER
Definition: UserRequest.cc:24
static const auto KeyPeriodLengthConfigured
int optind
Definition: getopt.c:48
SBuf ToSBuf(Args &&... args)
slowly stream-prints all arguments into a freshly allocated SBuf
Definition: Stream.h:63
static void writeTime(const char *user_key, const char *sub_key, time_t t)
static void shutdown_db(void)
#define DBG_IMPORTANT
Definition: Stream.h:38
static void init_db(void)
#define debugs(SECTION, LEVEL, CONTENT)
Definition: Stream.h:192
#define HLP_MSG(text)
#define SEND_OK(x)
static const auto KeyPeriodStart

 

Introduction

Documentation

Support

Miscellaneous