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