--- /dev/null
+.if !'po4a'hide' .TH ext_time_quota_acl 8 "22 March 2011"
+.
+.SH NAME
+.if !'po4a'hide' .B ext_time_quota_acl
+.if !'po4a'hide' \-
+Squid time quota external acl helper.
+.PP
+Version 1.0
+.
+.SH SYNOPSIS
+.if !'po4a'hide' .B ext_time_quota_acl
+.if !'po4a'hide' .B "[\-b database] [\-l logfile] [\-d] [\-p pauselen] [\-h] configfile
+.
+.SH DESCRIPTION
+.B ext_time_quota_acl
+allows an administrator to define time budgets for the users of squid
+to limit the time using squid.
+.PP
+This is useful for corporate lunch time allocations, wifi portal
+pay-per-minute installations or for parental control of children. The
+administrator can define a time budget (e.g. 1 hour per day) which is enforced
+through this helper.
+.
+.SH OPTIONS
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-b database"
+.B Filename
+of persistent database. This defaults to ext_time_quota.db in Squids state
+directory.
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-p pauselen"
+.B Pauselen
+is given in seconds and defines the period between two requests to be treated as part of the same session.
+Pauses shorter than this value will be counted against the quota, longer ones ignored.
+Default is 300 seconds (5 minutes).
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-l logfile"
+.B Filename
+where all logging and debugging information will be written. If none is given,
+then stderr will be used and the logging will go to Squids main cache.log.
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-d"
+Enables debug logging in the logfile.
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B "\-h"
+show a short command line help.
+.
+.if !'po4a'hide' .TP
+.if !'po4a'hide' .B configfile
+This file contains the definition of the time budgets for the users.
+.PP
+.
+.SH CONFIGURATION
+.PP
+The time quotas of the users are defined in a text file typically
+residing in /etc/squid/time_quota. Any line starting with "#" contains
+a comment and is ignored. Every line must start with a user
+followed by a time budget and a corresponding time period separated by
+"/". Here is an example file:
+.PP
+.if !'po4a'hide' .RS
+# user budget / period
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B john 8h / 1d
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B littlejoe 1h / 1d
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B babymary 30m / 1w
+.if !'po4a'hide' .br
+.if !'po4a'hide' .RE
+.PP
+John has a time budget of 8 hours every day, littlejoe is only allowed
+1 hour and the poor babymary only 30 minutes a week.
+.PP
+You can use "s" for seconds, "m" for minutes, "h" for hours, "d" for
+days and "w" for weeks. Numerical values can be given as integer
+values or with a fraction. E.g. "0.5h" means 30 minutes.
+.PP
+This helper is configured in
+.B squid.conf
+using the
+.B external_acl_type
+directive then access controls which use it to allow or deny.
+.
+.PP
+Here is an example.
+.PP
+.if !'po4a'hide' .RS
+# Ensure that users have a valid login. We need their username.
+.if !'po4a'hide' .br
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B acl users proxy_auth REQUIRED
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B http_access deny !users
+.if !'po4a'hide' .br
+# Define program and quota file
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B external_acl_type time_quota ttl=60 children-max=1 %LOGIN /usr/libexec/ext_time_quota_acl /etc/squid/time_quota
+.if !'po4a'hide' .br
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B acl noquota src all
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B acl time_quota external time_quota
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B deny_info ERR_ACL_TIME_QUOTA_EXCEEDED noquota
+.if !'po4a'hide' .br
+.if !'po4a'hide' .B http_access deny !time_quota noquota
+.if !'po4a'hide' .RE
+.
+.PP
+In this example, after restarting Squid it should allow access only for users as long as they have time budget left.
+If the budget is exceeded the user will be presented with an error page informing them.
+.PP
+In this example we use separate
+.B users
+access control and
+.B noquota
+ACL in order to keep the username and password prompt and the quota-exceeded messages separated.
+.
+.PP
+User is just a unique key value. The above example uses %LOGIN and the username but any of the
+.B external_acl_type
+format tags can be substituted in its place.
+.B %EXT_TAG
+,
+.B %LOGIN
+,
+.B %IDENT
+,
+.B %EXT_USER
+,
+.B %SRC
+,
+.B %SRCEUI48
+, and
+.B %SRCEUI64
+are all likely candidates for client identification.
+The Squid wiki has more examples at http://wiki.squid-cache.org/ConfigExamples.
+.
+.SH LIMITATIONS
+.PP
+This helper only controls access to the Internet through HTTP. It does
+not control other protocols, like VOIP, ICQ, IRC, FTP, IMAP, SMTP or
+SSH.
+.
+.PP
+Desktop browsers are typically able to deal with authentication to HTTP proxies like
+.B squid .
+But more and more different programs and devices (smartphones,
+games on mobile devices, ...) are using the Internet over HTTP. These
+devices are often not able to work through an authenticating proxy.
+Means other than %LOGIN authentication are required to authorize these devices and software.
+.
+.PP
+A more general control to Internet access could be a captive portal approach
+(such as pfSense or ChilliSpot) using %SRC, %SRCEUI48 and %SRCEUI64 as keys
+or maybe a 802.11X solution. But the latter is often not supported by mobile devices.
+.
+.SH IMPLEMENTATION
+.PP
+When the helper is called it will be asked if the current user is allowed to
+access squid. The helper will reduce the remaining time budget of this user
+and return
+.B OK
+if there is budget left. Otherwise it will return
+.B ERR .
+.
+.PP
+The
+.B ttl=N
+parameter in
+.B squid.conf
+determines how often the helper will be called, the example config uses a 1 minute TTL.
+The interaction is that Squid will only call the helper on new requests
+.B if
+there has been more than TTL seconds passed since last check.
+This handling creates an amount of slippage outside the quota by whatever amount is configured.
+TTL can be set as short as desired, down to and including zero.
+Though values of 1 or more are recommended due to a quota resolution of one second.
+.
+.PP
+If the configured time period (e.g. "1w" for babymary) is over, the
+time budget will be restored to the configured value thus allowing the
+user to access squid with a fresh budget.
+.
+.PP
+If the time between the current request and the previous request is greater than
+.B pauselen
+(default 5 minutes and adjustable with command line parameter
+.B "-p"
+), the current request will be considered as a new request and the time budget will
+not be decreased. If the time is less than
+.B pauselen
+, then both requests will be considered as part of the same active time period and the time budget will
+be decreased by the time difference. This allows the user to take arbitrary
+breaks during Internet access without losing their time budget.
+.
+.SH FURTHER IDEAS
+The following ideas could further improve this helper. Maybe someone
+wants to help? Any support or feedback is welcome!
+.if !'po4a'hide' .TP
+There should be a way for a user to see their configured and remaining
+time budget. This could be realized by implementing a web page
+accessing the database of the helper showing the corresponding
+data. One of the problems to be solved is user authentication.
+.if !'po4a'hide' .TP
+We could always return "OK" and use the module simply as an Internet
+usage tracker showing who has stayed how long in the WWW.
+.PP
+.
+.SH AUTHOR
+This program and documentation was written by
+.if !'po4a'hide' .I Dr. Tilmann Bubeck <t.bubeck@reinform.de>
+.
+.SH COPYRIGHT
+This program and documentation is copyright to the authors named above.
+.PP
+Distributed under the GNU General Public License (GNU GPL) version 2 or later (GPLv2+).
+.
+.SH QUESTIONS
+Questions on the usage of this program can be sent to the
+.I Squid Users mailing list
+.if !'po4a'hide' <squid-users@squid-cache.org>
+.
+.SH REPORTING BUGS
+Bug reports need to be made in English.
+See http://wiki.squid-cache.org/SquidFaq/BugReporting for details of what you need to include with your bug report.
+.PP
+Report bugs or bug fixes using http://bugs.squid-cache.org/
+.PP
+Report serious security bugs to
+.I Squid Bugs <squid-bugs@squid-cache.org>
+.PP
+Report ideas for new improvements to the
+.I Squid Developers mailing list
+.if !'po4a'hide' <squid-dev@squid-cache.org>
+.
+.SH SEE ALSO
+.if !'po4a'hide' .BR squid "(8), "
+.if !'po4a'hide' .BR GPL "(7), "
+.br
+The Squid FAQ wiki
+.if !'po4a'hide' http://wiki.squid-cache.org/SquidFaq
+.br
+The Squid Configuration Manual
+.if !'po4a'hide' http://www.squid-cache.org/Doc/config/
--- /dev/null
+/*
+ * ext_time_quota_acl: Squid external acl helper for quota on usage.
+ *
+ * Copyright (C) 2011 Dr. Tilmann Bubeck <t.bubeck@reinform.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
+ */
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "helpers/defines.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#if HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#include <string.h>
+#include <time.h>
+#if HAVE_GETOPT_H
+#include <getopt.h>
+#endif
+
+/* At this point all Bit Types are already defined, so we must
+ protect from multiple type definition on platform where
+ __BIT_TYPES_DEFINED__ is not defined.
+ */
+#ifndef __BIT_TYPES_DEFINED__
+#define __BIT_TYPES_DEFINED__
+#endif
+
+#if HAVE_DB_185_H
+#include <db_185.h>
+#elif HAVE_DB_H
+#include <db.h>
+#endif
+
+#ifndef DEFAULT_QUOTA_DB
+#error "Please define DEFAULT_QUOTA_DB preprocessor constant."
+#endif
+
+const char *db_path = DEFAULT_QUOTA_DB;
+const char *program_name;
+
+DB *db = NULL;
+
+#define KEY_LAST_ACTIVITY "last-activity"
+#define KEY_PERIOD_START "period-start"
+#define KEY_PERIOD_LENGTH_CONFIGURED "period-length-configured"
+#define KEY_TIME_BUDGET_LEFT "time-budget-left"
+#define KEY_TIME_BUDGET_CONFIGURED "time-budget-configured"
+
+/** Maximum size of buffers used to read or display lines. */
+#define TQ_BUFFERSIZE 1024
+
+/** If there is more than this given number of seconds between two
+ * sucessive requests, than the second request will be treated as a
+ * new request and the time between first and seconds request will
+ * be treated as a activity pause.
+ *
+ * Otherwise the following request will be treated as belonging to the
+ * same activity and the quota will be reduced.
+ */
+static int pauseLength = 300;
+
+static FILE *logfile = stderr;
+static int tq_debug_enabled = false;
+
+static void open_log(const char *logfilename) {
+ logfile = fopen(logfilename, "a");
+ if ( logfile == NULL ) {
+ perror(logfilename);
+ logfile = stderr;
+ }
+}
+
+static void vlog(const char *level, const char *format, va_list args)
+{
+ time_t now = time(NULL);
+
+ fprintf(logfile, "%ld %s| %s: ", now, program_name, level);
+ vfprintf (logfile, format, args);
+ fflush(logfile);
+}
+
+static void log_debug(const char *format, ...)
+{
+ va_list args;
+
+ if ( tq_debug_enabled ) {
+ va_start (args, format);
+ vlog("DEBUG", format, args);
+ va_end (args);
+ }
+}
+
+static void log_info(const char *format, ...)
+{
+ va_list args;
+
+ va_start (args, format);
+ vlog("INFO", format, args);
+ va_end (args);
+}
+
+static void log_error(const char *format, ...)
+{
+ va_list args;
+
+ va_start (args, format);
+ vlog("ERROR", format, args);
+ va_end (args);
+}
+
+static void log_fatal(const char *format, ...)
+{
+ va_list args;
+
+ va_start (args, format);
+ vlog("FATAL", format, args);
+ va_end (args);
+}
+
+static void init_db(void)
+{
+ log_info("opening time quota database \"%s\".\n", db_path);
+ db = dbopen(db_path, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL);
+ if (!db) {
+ log_fatal("Failed to open time_quota db '%s'\n", db_path);
+ exit(1);
+ }
+}
+
+static void shutdown_db(void)
+{
+ db->close(db);
+}
+
+static void writeTime(const char *user_key, const char *sub_key, time_t t)
+{
+ char keybuffer[TQ_BUFFERSIZE];
+ DBT key, data;
+
+ if ( strlen(user_key) + strlen(sub_key) + 1 + 1 > sizeof(keybuffer) ) {
+ log_error("key too long (%s,%s)\n", user_key, sub_key);
+ } else {
+ snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
+
+ key.data = (void *)keybuffer;
+ key.size = strlen(keybuffer);
+ data.data = &t;
+ data.size = sizeof(t);
+ db->put(db, &key, &data, 0);
+ log_debug("writeTime(\"%s\", %d)\n", keybuffer, t);
+ }
+}
+
+static time_t readTime(const char *user_key, const char *sub_key)
+{
+ char keybuffer[TQ_BUFFERSIZE];
+ DBT key, data;
+ time_t t = 0;
+
+ if ( strlen(user_key) + 1 + strlen(sub_key) + 1 > sizeof(keybuffer) ) {
+ log_error("key too long (%s,%s)\n", user_key, sub_key);
+ } else {
+ snprintf(keybuffer, sizeof(keybuffer), "%s-%s", user_key, sub_key);
+
+ key.data = (void *)keybuffer;
+ key.size = strlen(keybuffer);
+ if (db->get(db, &key, &data, 0) == 0) {
+ if (data.size != sizeof(t)) {
+ log_error("CORRUPTED DATABASE (%s)\n", keybuffer);
+ } else {
+ memcpy(&t, data.data, sizeof(t));
+ }
+ }
+ log_debug("readTime(\"%s\")=%d\n", keybuffer, t);
+ }
+
+ return t;
+}
+
+static void parseTime(const char *s, time_t *secs, time_t *start)
+{
+ double value;
+ char unit;
+ struct tm *ltime;
+ int periodLength = 3600;
+
+ *secs = 0;
+ *start = time(NULL);
+ ltime = localtime(start);
+
+ sscanf(s, " %lf %c", &value, &unit);
+ switch (unit) {
+ case 's':
+ periodLength = 1;
+ break;
+ case 'm':
+ periodLength = 60;
+ *start -= ltime->tm_sec;
+ break;
+ case 'h':
+ periodLength = 3600;
+ *start -= ltime->tm_min * 60 + ltime->tm_sec;
+ break;
+ case 'd':
+ periodLength = 24 * 3600;
+ *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
+ break;
+ case 'w':
+ periodLength = 7 * 24 * 3600;
+ *start -= ltime->tm_hour * 3600 + ltime->tm_min * 60 + ltime->tm_sec;
+ *start -= ltime->tm_wday * 24 * 3600;
+ *start += 24 * 3600; // in europe, the week starts monday
+ break;
+ default:
+ log_error("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n", unit);
+ break;
+ }
+
+ *secs = (long)(periodLength * value);
+}
+
+
+/** This function parses the time quota file and stores it
+ * in memory.
+ */
+static void readConfig(const char *filename)
+{
+ char line[TQ_BUFFERSIZE]; /* the buffer for the lines read
+ from the dict file */
+ char *cp; /* a char pointer used to parse
+ each line */
+ char *username; /* for the username */
+ char *budget;
+ char *period;
+ FILE *FH;
+ time_t t;
+ time_t budgetSecs, periodSecs;
+ time_t start;
+
+ log_info("reading config file \"%s\".\n", filename);
+
+ FH = fopen(filename, "r");
+ if ( FH ) {
+ /* the pointer to the first entry in the linked list */
+ while ((cp = fgets (line, sizeof(line), FH)) != NULL) {
+ if (line[0] == '#') {
+ continue;
+ }
+ if ((cp = strchr (line, '\n')) != NULL) {
+ /* chop \n characters */
+ *cp = '\0';
+ }
+ log_debug("read config line \"%s\".\n", line);
+ if ((cp = strtok (line, "\t ")) != NULL) {
+ username = cp;
+
+ /* get the time budget */
+ budget = strtok (NULL, "/");
+ period = strtok (NULL, "/");
+
+ parseTime(budget, &budgetSecs, &start);
+ parseTime(period, &periodSecs, &start);
+
+ log_debug("read time quota for user \"%s\": %lds / %lds starting %lds\n",
+ username, budgetSecs, periodSecs, start);
+
+ writeTime(username, KEY_PERIOD_START, start);
+ writeTime(username, KEY_PERIOD_LENGTH_CONFIGURED, periodSecs);
+ writeTime(username, KEY_TIME_BUDGET_CONFIGURED, budgetSecs);
+ t = readTime(username, KEY_TIME_BUDGET_CONFIGURED);
+ writeTime(username, KEY_TIME_BUDGET_LEFT, t);
+ }
+ }
+ fclose(FH);
+ } else {
+ perror(filename);
+ }
+}
+
+static void processActivity(const char *user_key)
+{
+ time_t now = time(NULL);
+ time_t lastActivity;
+ time_t activityLength;
+ time_t periodStart;
+ time_t periodLength;
+ time_t userPeriodLength;
+ time_t timeBudgetCurrent;
+ time_t timeBudgetConfigured;
+ char message[TQ_BUFFERSIZE];
+
+ log_debug("processActivity(\"%s\")\n", user_key);
+
+ // [1] Reset period if over
+ periodStart = readTime(user_key, KEY_PERIOD_START);
+ if ( periodStart == 0 ) {
+ // This is the first period ever.
+ periodStart = now;
+ writeTime(user_key, KEY_PERIOD_START, periodStart);
+ }
+
+ periodLength = now - periodStart;
+ userPeriodLength = readTime(user_key, KEY_PERIOD_LENGTH_CONFIGURED);
+ if ( userPeriodLength == 0 ) {
+ // This user is not configured. Allow anything.
+ log_debug("No period length found for user \"%s\". Quota for this user disabled.\n", user_key);
+ writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
+ } else {
+ if ( periodLength >= userPeriodLength ) {
+ // a new period has started.
+ log_debug("New time period started for user \"%s\".\n", user_key);
+ while ( periodStart < now ) {
+ periodStart += periodLength;
+ }
+ writeTime(user_key, KEY_PERIOD_START, periodStart);
+ timeBudgetConfigured = readTime(user_key, KEY_TIME_BUDGET_CONFIGURED);
+ if ( timeBudgetConfigured == 0 ) {
+ log_debug("No time budget configured for user \"%s\". Quota for this user disabled.\n", user_key);
+ writeTime(user_key, KEY_TIME_BUDGET_LEFT, pauseLength);
+ } else {
+ writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetConfigured);
+ }
+ }
+ }
+
+ // [2] Decrease time budget iff activity
+ lastActivity = readTime(user_key, KEY_LAST_ACTIVITY);
+ if ( lastActivity == 0 ) {
+ // This is the first request ever
+ writeTime(user_key, KEY_LAST_ACTIVITY, now);
+ } else {
+ activityLength = now - lastActivity;
+ if ( activityLength >= pauseLength ) {
+ // This is an activity pause.
+ log_debug("Activity pause detected for user \"%s\".\n", user_key);
+ writeTime(user_key, KEY_LAST_ACTIVITY, now);
+ } else {
+ // This is real usage.
+ writeTime(user_key, KEY_LAST_ACTIVITY, now);
+
+ log_debug("Time budget reduced by %ld for user \"%s\".\n",
+ activityLength, user_key);
+ timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
+ timeBudgetCurrent -= activityLength;
+ writeTime(user_key, KEY_TIME_BUDGET_LEFT, timeBudgetCurrent);
+ }
+ }
+
+ timeBudgetCurrent = readTime(user_key, KEY_TIME_BUDGET_LEFT);
+ snprintf(message, TQ_BUFFERSIZE, "message=\"Remaining quota for '%s' is %d seconds.\"", user_key, (int)timeBudgetCurrent);
+ if ( timeBudgetCurrent > 0 ) {
+ log_debug("OK %s.\n", message);
+ SEND_OK(message);
+ } else {
+ log_debug("ERR %s\n", message);
+ SEND_ERR("Time budget exceeded.");
+ }
+
+ db->sync(db, 0);
+}
+
+static void usage(void)
+{
+ log_error("Wrong usage. Please reconfigure in squid.conf.\n");
+
+ fprintf(stderr, "Usage: %s [-d] [-l logfile] [-b dbpath] [-p pauselen] [-h] configfile\n", program_name);
+ fprintf(stderr, " -d enable debugging output to logfile\n");
+ fprintf(stderr, " -l logfile log messages to logfile\n");
+ fprintf(stderr, " -b dbpath Path where persistent session database will be kept\n");
+ fprintf(stderr, " If option is not used, then " DEFAULT_QUOTA_DB " will be used.\n");
+ fprintf(stderr, " -p pauselen length in seconds to describe a pause between 2 requests.\n");
+ fprintf(stderr, " -h show show command line help.\n");
+ fprintf(stderr, "configfile is a file containing time quota definitions.\n");
+}
+
+int main(int argc, char **argv)
+{
+ char request[HELPER_INPUT_BUFFER];
+ int opt;
+
+ program_name = argv[0];
+
+ while ((opt = getopt(argc, argv, "dp:l:b:h")) != -1) {
+ switch (opt) {
+ case 'd':
+ tq_debug_enabled = true;
+ break;
+ case 'l':
+ open_log(optarg);
+ break;
+ case 'b':
+ db_path = optarg;
+ break;
+ case 'p':
+ pauseLength = atoi(optarg);
+ break;
+ case 'h':
+ usage();
+ exit(0);
+ break;
+ }
+ }
+
+ log_info("Starting %s\n", __FILE__);
+ setbuf(stdout, NULL);
+
+ init_db();
+
+ if ( optind + 1 != argc ) {
+ usage();
+ exit(1);
+ } else {
+ readConfig(argv[optind]);
+ }
+
+ log_info("Waiting for requests...\n");
+ while (fgets(request, HELPER_INPUT_BUFFER, stdin)) {
+ // we expect the following line syntax: "%LOGIN
+ const char *user_key = NULL;
+ user_key = strtok(request, " \n");
+
+ processActivity(user_key);
+ }
+ log_info("Ending %s\n", __FILE__);
+ shutdown_db();
+ return 0;
+}