]>
Commit | Line | Data |
---|---|---|
ca02e0ec | 1 | /* |
b8ae064d | 2 | * Copyright (C) 1996-2023 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 | ||
4878cfe5 FC |
29 | /* DEBUG: section 82 External ACL Helpers */ |
30 | ||
f7f3304a | 31 | #include "squid.h" |
4878cfe5 | 32 | #include "debug/Stream.h" |
079b1d0f | 33 | #include "helper/protocol_defines.h" |
4878cfe5 | 34 | #include "sbuf/Stream.h" |
9938b57f | 35 | |
074d6a40 | 36 | #include <ctime> |
9938b57f TB |
37 | #if HAVE_GETOPT_H |
38 | #include <getopt.h> | |
39 | #endif | |
acd207af AJ |
40 | #if HAVE_TDB_H |
41 | #include <tdb.h> | |
9938b57f TB |
42 | #endif |
43 | ||
44 | #ifndef DEFAULT_QUOTA_DB | |
45 | #error "Please define DEFAULT_QUOTA_DB preprocessor constant." | |
46 | #endif | |
47 | ||
4878cfe5 | 48 | static const auto MY_DEBUG_SECTION = 82; |
9938b57f TB |
49 | const char *db_path = DEFAULT_QUOTA_DB; |
50 | const char *program_name; | |
51 | ||
acd207af | 52 | TDB_CONTEXT *db = nullptr; |
9938b57f | 53 | |
4878cfe5 FC |
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"; | |
9938b57f TB |
59 | |
60 | /** Maximum size of buffers used to read or display lines. */ | |
4878cfe5 | 61 | static const size_t TQ_BUFFERSIZE = 1024; |
9938b57f TB |
62 | |
63 | /** If there is more than this given number of seconds between two | |
2f8abb64 | 64 | * successive requests, than the second request will be treated as a |
9938b57f TB |
65 | * new request and the time between first and seconds request will |
66 | * be treated as a activity pause. | |
67 | * | |
68 | * Otherwise the following request will be treated as belonging to the | |
69 | * same activity and the quota will be reduced. | |
70 | */ | |
71 | static int pauseLength = 300; | |
72 | ||
9938b57f TB |
73 | static void init_db(void) |
74 | { | |
4878cfe5 FC |
75 | debugs(MY_DEBUG_SECTION, 2, "opening time quota database \"" << db_path << "\"."); |
76 | ||
acd207af | 77 | db = tdb_open(db_path, 0, TDB_CLEAR_IF_FIRST, O_CREAT | O_RDWR, 0666); |
9938b57f | 78 | if (!db) { |
4878cfe5 | 79 | debugs(MY_DEBUG_SECTION, DBG_CRITICAL, "FATAL: Failed to open time_quota db '" << db_path << '\''); |
24885773 | 80 | exit(EXIT_FAILURE); |
9938b57f | 81 | } |
4878cfe5 FC |
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"); | |
9938b57f TB |
84 | } |
85 | ||
86 | static void shutdown_db(void) | |
87 | { | |
acd207af | 88 | tdb_close(db); |
9938b57f TB |
89 | } |
90 | ||
4878cfe5 | 91 | static SBuf KeyString(const char *user_key, const char *sub_key) |
9938b57f | 92 | { |
4878cfe5 | 93 | return ToSBuf(user_key, "-", sub_key); |
acd207af AJ |
94 | } |
95 | ||
96 | static void writeTime(const char *user_key, const char *sub_key, time_t t) | |
97 | { | |
4878cfe5 FC |
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 << ')'); | |
9938b57f TB |
110 | } |
111 | ||
112 | static time_t readTime(const char *user_key, const char *sub_key) | |
113 | { | |
4878cfe5 FC |
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 | } | |
acd207af | 125 | |
4878cfe5 FC |
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); | |
9938b57f TB |
134 | } |
135 | ||
4878cfe5 FC |
136 | debugs(MY_DEBUG_SECTION, 3, "readTime(\"" << ks << "\")=" << t); |
137 | return t; | |
9938b57f TB |
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': | |
f8901ea9 A |
154 | periodLength = 1; |
155 | break; | |
9938b57f | 156 | case 'm': |
f8901ea9 A |
157 | periodLength = 60; |
158 | *start -= ltime->tm_sec; | |
159 | break; | |
9938b57f | 160 | case 'h': |
f8901ea9 A |
161 | periodLength = 3600; |
162 | *start -= ltime->tm_min * 60 + ltime->tm_sec; | |
163 | break; | |
9938b57f | 164 | case 'd': |
f8901ea9 A |
165 | periodLength = 24 * 3600; |
166 | *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec; | |
167 | break; | |
9938b57f | 168 | case 'w': |
f8901ea9 A |
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; | |
9938b57f | 174 | default: |
4878cfe5 | 175 | debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: Wrong time unit \"" << unit << "\". Only \"m\", \"h\", \"d\", or \"w\" allowed"); |
f8901ea9 | 176 | break; |
9938b57f TB |
177 | } |
178 | ||
179 | *secs = (long)(periodLength * value); | |
180 | } | |
181 | ||
9938b57f TB |
182 | /** This function parses the time quota file and stores it |
183 | * in memory. | |
184 | */ | |
f8901ea9 | 185 | static void readConfig(const char *filename) |
9938b57f TB |
186 | { |
187 | char line[TQ_BUFFERSIZE]; /* the buffer for the lines read | |
f53969cc SM |
188 | from the dict file */ |
189 | char *cp; /* a char pointer used to parse | |
190 | each line */ | |
191 | char *username; /* for the username */ | |
9938b57f TB |
192 | char *budget; |
193 | char *period; | |
194 | FILE *FH; | |
195 | time_t t; | |
196 | time_t budgetSecs, periodSecs; | |
197 | time_t start; | |
198 | ||
4878cfe5 | 199 | debugs(MY_DEBUG_SECTION, 2, "reading config file \"" << filename << "\"."); |
9938b57f TB |
200 | |
201 | FH = fopen(filename, "r"); | |
202 | if ( FH ) { | |
f8901ea9 | 203 | /* the pointer to the first entry in the linked list */ |
03f581b0 AJ |
204 | unsigned int lineCount = 0; |
205 | while (fgets(line, sizeof(line), FH)) { | |
206 | ++lineCount; | |
f8901ea9 A |
207 | if (line[0] == '#') { |
208 | continue; | |
209 | } | |
210 | if ((cp = strchr (line, '\n')) != NULL) { | |
211 | /* chop \n characters */ | |
212 | *cp = '\0'; | |
213 | } | |
4878cfe5 | 214 | debugs(MY_DEBUG_SECTION, 3, "read config line " << lineCount << ": \"" << line << '\"'); |
03f581b0 | 215 | if ((username = strtok(line, "\t ")) != NULL) { |
f8901ea9 A |
216 | |
217 | /* get the time budget */ | |
a1b1756c | 218 | if ((budget = strtok(nullptr, "/")) == NULL) { |
4878cfe5 | 219 | debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: missing 'budget' field on line " << lineCount << " of '" << filename << '\''); |
03f581b0 AJ |
220 | continue; |
221 | } | |
a1b1756c | 222 | if ((period = strtok(nullptr, "/")) == NULL) { |
4878cfe5 | 223 | debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "ERROR: missing 'period' field on line " << lineCount << " of '" << filename << '\''); |
03f581b0 AJ |
224 | continue; |
225 | } | |
f8901ea9 A |
226 | |
227 | parseTime(budget, &budgetSecs, &start); | |
228 | parseTime(period, &periodSecs, &start); | |
229 | ||
4878cfe5 FC |
230 | debugs(MY_DEBUG_SECTION, 3, "read time quota for user \"" << username << "\": " << |
231 | budgetSecs << "s / " << periodSecs << "s starting " << start); | |
f8901ea9 | 232 | |
4878cfe5 FC |
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); | |
f8901ea9 A |
238 | } |
239 | } | |
240 | fclose(FH); | |
9938b57f | 241 | } else { |
f8901ea9 | 242 | perror(filename); |
9938b57f TB |
243 | } |
244 | } | |
245 | ||
246 | static void processActivity(const char *user_key) | |
247 | { | |
f8901ea9 A |
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; | |
f8901ea9 | 256 | |
4878cfe5 | 257 | debugs(MY_DEBUG_SECTION, 3, "processActivity(\"" << user_key << "\")"); |
f8901ea9 A |
258 | |
259 | // [1] Reset period if over | |
4878cfe5 | 260 | periodStart = readTime(user_key, KeyPeriodStart); |
f8901ea9 A |
261 | if ( periodStart == 0 ) { |
262 | // This is the first period ever. | |
263 | periodStart = now; | |
4878cfe5 | 264 | writeTime(user_key, KeyPeriodStart, periodStart); |
f8901ea9 A |
265 | } |
266 | ||
267 | periodLength = now - periodStart; | |
4878cfe5 | 268 | userPeriodLength = readTime(user_key, KeyPeriodLengthConfigured); |
f8901ea9 A |
269 | if ( userPeriodLength == 0 ) { |
270 | // This user is not configured. Allow anything. | |
4878cfe5 FC |
271 | debugs(MY_DEBUG_SECTION, 3, "disabling user quota for user '" << |
272 | user_key << "': no period length found"); | |
273 | writeTime(user_key, KeyTimeBudgetLeft, pauseLength); | |
f8901ea9 A |
274 | } else { |
275 | if ( periodLength >= userPeriodLength ) { | |
276 | // a new period has started. | |
4878cfe5 | 277 | debugs(MY_DEBUG_SECTION, 3, "New time period started for user \"" << user_key << '\"'); |
f8901ea9 A |
278 | while ( periodStart < now ) { |
279 | periodStart += periodLength; | |
280 | } | |
4878cfe5 FC |
281 | writeTime(user_key, KeyPeriodStart, periodStart); |
282 | timeBudgetConfigured = readTime(user_key, KeyTimeBudgetConfigured); | |
f8901ea9 | 283 | if ( timeBudgetConfigured == 0 ) { |
4878cfe5 FC |
284 | debugs(MY_DEBUG_SECTION, 3, "No time budget configured for user \"" << user_key << |
285 | "\". Quota for this user disabled."); | |
286 | writeTime(user_key, KeyTimeBudgetLeft, pauseLength); | |
f8901ea9 | 287 | } else { |
4878cfe5 | 288 | writeTime(user_key, KeyTimeBudgetLeft, timeBudgetConfigured); |
f8901ea9 A |
289 | } |
290 | } | |
291 | } | |
292 | ||
293 | // [2] Decrease time budget iff activity | |
4878cfe5 | 294 | lastActivity = readTime(user_key, KeyLastActivity); |
f8901ea9 A |
295 | if ( lastActivity == 0 ) { |
296 | // This is the first request ever | |
4878cfe5 | 297 | writeTime(user_key, KeyLastActivity, now); |
f8901ea9 A |
298 | } else { |
299 | activityLength = now - lastActivity; | |
300 | if ( activityLength >= pauseLength ) { | |
301 | // This is an activity pause. | |
4878cfe5 FC |
302 | debugs(MY_DEBUG_SECTION, 3, "Activity pause detected for user \"" << user_key << "\"."); |
303 | writeTime(user_key, KeyLastActivity, now); | |
f8901ea9 A |
304 | } else { |
305 | // This is real usage. | |
4878cfe5 | 306 | writeTime(user_key, KeyLastActivity, now); |
f8901ea9 | 307 | |
4878cfe5 FC |
308 | debugs(MY_DEBUG_SECTION, 3, "Time budget reduced by " << activityLength << |
309 | " for user \"" << user_key << "\"."); | |
310 | timeBudgetCurrent = readTime(user_key, KeyTimeBudgetLeft); | |
f8901ea9 | 311 | timeBudgetCurrent -= activityLength; |
4878cfe5 | 312 | writeTime(user_key, KeyTimeBudgetLeft, timeBudgetCurrent); |
f8901ea9 A |
313 | } |
314 | } | |
315 | ||
4878cfe5 FC |
316 | timeBudgetCurrent = readTime(user_key, KeyTimeBudgetLeft); |
317 | ||
318 | const auto message = ToSBuf(HLP_MSG("Remaining quota for '"), user_key, "' is ", timeBudgetCurrent, " seconds."); | |
f8901ea9 | 319 | if ( timeBudgetCurrent > 0 ) { |
f8901ea9 A |
320 | SEND_OK(message); |
321 | } else { | |
f8901ea9 A |
322 | SEND_ERR("Time budget exceeded."); |
323 | } | |
9938b57f TB |
324 | } |
325 | ||
326 | static void usage(void) | |
327 | { | |
4878cfe5 FC |
328 | debugs(MY_DEBUG_SECTION, DBG_CRITICAL, "Wrong usage. Please reconfigure in squid.conf."); |
329 | ||
330 | std::cerr << | |
331 | "Usage: " << program_name << " [-d] [-b dbpath] [-p pauselen] [-h] configfile\n" | |
332 | " -d enable debugging output\n" | |
333 | " -l logfile log messages to logfile\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"; | |
9938b57f TB |
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]; | |
4878cfe5 | 347 | Debug::NameThisHelper("ext_time_quota_acl"); |
9938b57f | 348 | |
2d93cfe7 | 349 | while ((opt = getopt(argc, argv, "dp:b:h")) != -1) { |
9938b57f TB |
350 | switch (opt) { |
351 | case 'd': | |
4878cfe5 | 352 | Debug::Levels[MY_DEBUG_SECTION] = DBG_DATA; |
9938b57f | 353 | break; |
9938b57f TB |
354 | case 'b': |
355 | db_path = optarg; | |
356 | break; | |
357 | case 'p': | |
358 | pauseLength = atoi(optarg); | |
359 | break; | |
360 | case 'h': | |
361 | usage(); | |
24885773 | 362 | exit(EXIT_SUCCESS); |
9938b57f | 363 | break; |
2d93cfe7 FC |
364 | default: |
365 | // getopt() emits error message to stderr | |
366 | usage(); | |
367 | exit(EXIT_FAILURE); | |
368 | break; | |
9938b57f TB |
369 | } |
370 | } | |
371 | ||
4878cfe5 | 372 | debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "Starting " << program_name); |
a1b1756c | 373 | setbuf(stdout, nullptr); |
9938b57f TB |
374 | |
375 | init_db(); | |
376 | ||
377 | if ( optind + 1 != argc ) { | |
f8901ea9 | 378 | usage(); |
24885773 | 379 | exit(EXIT_FAILURE); |
9938b57f | 380 | } else { |
f8901ea9 | 381 | readConfig(argv[optind]); |
9938b57f TB |
382 | } |
383 | ||
4878cfe5 | 384 | debugs(MY_DEBUG_SECTION, 2, "Waiting for requests..."); |
9938b57f | 385 | while (fgets(request, HELPER_INPUT_BUFFER, stdin)) { |
03f581b0 AJ |
386 | // we expect the following line syntax: %LOGIN |
387 | const char *user_key = strtok(request, " \n"); | |
388 | if (!user_key) { | |
194ccc9c | 389 | SEND_BH(HLP_MSG("User name missing")); |
03f581b0 AJ |
390 | continue; |
391 | } | |
f8901ea9 | 392 | processActivity(user_key); |
9938b57f | 393 | } |
4878cfe5 | 394 | debugs(MY_DEBUG_SECTION, DBG_IMPORTANT, "Ending " << program_name); |
9938b57f | 395 | shutdown_db(); |
24885773 | 396 | return EXIT_SUCCESS; |
9938b57f | 397 | } |
f53969cc | 398 |