Squid Web Cache master
Loading...
Searching...
No Matches
ext_time_quota_acl.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) 1996-2025 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
48static const auto MY_DEBUG_SECTION = 82;
49const char *db_path = DEFAULT_QUOTA_DB;
50const char *program_name;
51
52TDB_CONTEXT *db = nullptr;
53
54static const auto KeyLastActivity = "last-activity";
55static const auto KeyPeriodStart = "period-start";
56static const auto KeyPeriodLengthConfigured = "period-length-configured";
57static const auto KeyTimeBudgetLeft = "time-budget-left";
58static const auto KeyTimeBudgetConfigured = "time-budget-configured";
59
61static const size_t TQ_BUFFERSIZE = 1024;
62
71static int pauseLength = 300;
72
73static 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
86static void shutdown_db(void)
87{
88 tdb_close(db);
89}
90
91static SBuf KeyString(const char *user_key, const char *sub_key)
92{
93 return ToSBuf(user_key, "-", sub_key);
94}
95
96static 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
112static 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
140static 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
185static 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
246static 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
326static 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
341int 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
#define HELPER_INPUT_BUFFER
static void parseOptions(char const *)
Definition debug.cc:1095
static void NameThisHelper(const char *name)
Definition debug.cc:384
Definition SBuf.h:94
#define DBG_IMPORTANT
Definition Stream.h:38
#define debugs(SECTION, LEVEL, CONTENT)
Definition Stream.h:192
#define DBG_CRITICAL
Definition Stream.h:37
static const auto KeyPeriodStart
static const auto KeyTimeBudgetLeft
static void shutdown_db(void)
static const auto MY_DEBUG_SECTION
TDB_CONTEXT * db
static void processActivity(const char *user_key)
static const auto KeyTimeBudgetConfigured
static void writeTime(const char *user_key, const char *sub_key, time_t t)
static void init_db(void)
static time_t readTime(const char *user_key, const char *sub_key)
static void parseTime(const char *s, time_t *secs, time_t *start)
static SBuf KeyString(const char *user_key, const char *sub_key)
static const auto KeyLastActivity
const char * db_path
static const size_t TQ_BUFFERSIZE
static void readConfig(const char *filename)
static const auto KeyPeriodLengthConfigured
static void usage(void)
const char * program_name
static int pauseLength
int getopt(int nargc, char *const *nargv, const char *ostr)
Definition getopt.c:62
int optind
Definition getopt.c:48
char * optarg
Definition getopt.c:51
int main()
#define SEND_ERR(x)
#define SEND_OK(x)
#define HLP_MSG(text)
#define SEND_BH(x)
SBuf ToSBuf(Args &&... args)
slowly stream-prints all arguments into a freshly allocated SBuf
Definition Stream.h:63
#define NULL
Definition types.h:145