]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Add ext_time_quota_acl helper
authorTilmann Bubeck <t.bubeck@reinform.de>
Mon, 9 May 2011 12:42:59 +0000 (00:42 +1200)
committerAmos Jeffries <squid3@treenet.co.nz>
Mon, 9 May 2011 12:42:59 +0000 (00:42 +1200)
Allows an administrator to define time budgets for the users of squid
to limit the time using squid.

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.

configure.ac
errors/Makefile.am
errors/templates/ERR_ACL_TIME_QUOTA_EXCEEDED [new file with mode: 0644]
helpers/external_acl/Makefile.am
helpers/external_acl/time_quota/Makefile.am [new file with mode: 0644]
helpers/external_acl/time_quota/config.test [new file with mode: 0644]
helpers/external_acl/time_quota/ext_time_quota_acl.8 [new file with mode: 0644]
helpers/external_acl/time_quota/ext_time_quota_acl.cc [new file with mode: 0644]

index d274d7ca99f65420cc07047b8ad13c2a6c7a359c..23c0bab15f62df0c0753c55a827feb75444e9c27 100644 (file)
@@ -3451,6 +3451,7 @@ AC_CONFIG_FILES([\
        helpers/external_acl/session/Makefile \
        helpers/external_acl/unix_group/Makefile \
        helpers/external_acl/wbinfo_group/Makefile \
+       helpers/external_acl/time_quota/Makefile \
        helpers/log_daemon/Makefile \
        helpers/log_daemon/DB/Makefile \
        helpers/log_daemon/file/Makefile \
index 5bafab0a9db318789eb2dc500bfcb2e0d8b2fb4e..fefe53cd1e65f5a59e790e1d08ebeb464e77deee 100644 (file)
@@ -13,6 +13,7 @@ DEFAULT_STYLESHEET    = $(sysconfdir)/errorpage.css
 ## List of automated translations possible:
 ERROR_TEMPLATES =  \
        templates/ERR_ACCESS_DENIED \
+       templates/ERR_ACL_TIME_QUOTA_EXCEEDED \
        templates/ERR_CACHE_ACCESS_DENIED \
        templates/ERR_CACHE_MGR_ACCESS_DENIED \
        templates/ERR_CANNOT_FORWARD \
diff --git a/errors/templates/ERR_ACL_TIME_QUOTA_EXCEEDED b/errors/templates/ERR_ACL_TIME_QUOTA_EXCEEDED
new file mode 100644 (file)
index 0000000..cb95a4b
--- /dev/null
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<title>ERROR: The requested URL could not be retrieved</title>
+<style type="text/css"><!-- 
+ %l
+
+body
+:lang(fa) { direction: rtl; font-size: 100%; font-family: Tahoma, Roya, sans-serif; float: right; }
+:lang(he) { direction: rtl; float: right; }
+ --></style>
+</head><body>
+<div id="titles">
+<h1>ERROR</h1>
+<h2>The requested URL could not be retrieved</h2>
+</div>
+<hr>
+
+<div id="content">
+<p>The following error was encountered while trying to retrieve the URL: <a href="%U">%U</a></p>
+
+<blockquote id="error">
+<p><b>Time Quota Exceeded.</b></p>
+</blockquote>
+
+<p>This proxy limits your time online with a quota. Your time budget is now empty but will be refilled when the configured time period starts again.</p>
+<p>These limits have been established by the Internet Service Provider who operates this cache. Please contact them directly if you feel this is an error.</p>
+
+<p>Your cache administrator is <a href="mailto:%w%W">%w</a>.</p>
+<br>
+</div>
+
+<hr>
+<div id="footer">
+<p>Generated %T by %h (%s)</p>
+<!-- %c -->
+</div>
+</body></html>
index 1256ec95ed829ff3aec2b3518ba955e59fab3590..5cf235edfbb3e3fab61a9c0557fe2187182e81bd 100644 (file)
@@ -6,6 +6,7 @@ DIST_SUBDIRS= \
        LDAP_group \
        LM_group \
        session \
+       time_quota \
        unix_group \
        wbinfo_group
 
diff --git a/helpers/external_acl/time_quota/Makefile.am b/helpers/external_acl/time_quota/Makefile.am
new file mode 100644 (file)
index 0000000..a7bc65f
--- /dev/null
@@ -0,0 +1,12 @@
+include $(top_srcdir)/src/Common.am
+
+libexec_PROGRAMS               = ext_time_quota_acl
+man_MANS                       = ext_time_quota_acl.8
+EXTRA_DIST                     = ext_time_quota_acl.8 config.test
+ext_time_quota_acl_SOURCES     = ext_time_quota_acl.cc
+
+DEFS += -DDEFAULT_QUOTA_DB=\"$(localstatedir)/ext_time_quota.db\" 
+
+LDADD = \
+       $(COMPAT_LIB) \
+       $(LIB_DB)
diff --git a/helpers/external_acl/time_quota/config.test b/helpers/external_acl/time_quota/config.test
new file mode 100644 (file)
index 0000000..83b0f99
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Actual intended test
+if [ -f /usr/include/db_185.h ]; then
+    exit 0
+fi
+if [ -f /usr/include/db.h ] && grep dbopen /usr/include/db.h; then
+    exit 0
+fi
+exit 1
diff --git a/helpers/external_acl/time_quota/ext_time_quota_acl.8 b/helpers/external_acl/time_quota/ext_time_quota_acl.8
new file mode 100644 (file)
index 0000000..c53f6a7
--- /dev/null
@@ -0,0 +1,251 @@
+.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/
diff --git a/helpers/external_acl/time_quota/ext_time_quota_acl.cc b/helpers/external_acl/time_quota/ext_time_quota_acl.cc
new file mode 100644 (file)
index 0000000..fcc4ab5
--- /dev/null
@@ -0,0 +1,448 @@
+/*
+ * 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;
+}