diff -r -u -N trunk/configure.ac trunk-time_quota/configure.ac --- trunk/configure.ac 2011-04-04 21:08:49.000000000 +0200 +++ trunk-time_quota/configure.ac 2011-04-04 21:23:41.000000000 +0200 @@ -3434,6 +3434,7 @@ 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 \ diff -r -u -N trunk/configure.ac.orig trunk-time_quota/configure.ac.orig --- trunk/helpers/external_acl/Makefile.am 2011-03-19 17:23:31.000000000 +0100 +++ trunk-time_quota/helpers/external_acl/Makefile.am 2011-04-04 21:23:41.000000000 +0200 @@ -7,6 +7,7 @@ LM_group \ session \ unix_group \ - wbinfo_group + wbinfo_group \ + time_quota SUBDIRS=$(EXTERNAL_ACL_HELPERS) diff -r -u -N trunk/helpers/external_acl/time_quota/config.test trunk-time_quota/helpers/external_acl/time_quota/config.test --- trunk/helpers/external_acl/time_quota/config.test 1970-01-01 01:00:00.000000000 +0100 +++ trunk-time_quota/helpers/external_acl/time_quota/config.test 2011-03-19 17:25:01.000000000 +0100 @@ -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 -r -u -N trunk/helpers/external_acl/time_quota/ext_time_quota_acl.8 trunk-time_quota/helpers/external_acl/time_quota/ext_time_quota_acl.8 --- trunk/helpers/external_acl/time_quota/ext_time_quota_acl.8 1970-01-01 01:00:00.000000000 +0100 +++ trunk-time_quota/helpers/external_acl/time_quota/ext_time_quota_acl.8 2011-04-04 21:20:28.000000000 +0200 @@ -0,0 +1,274 @@ +.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] 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 +The primary use is for parental control for children. The parents 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. If not specified the available and used time +budgets will be kept in memory only and will reset each time Squid +restarts it's helpers (Squid restart or rotation of logs). +. +.if !'po4a'hide' .TP +.if !'po4a'hide' .B "\-l logfile" +.B Filename +to a log file where all logging and debugging information will be +written. +. +.if !'po4a'hide' .TP +.if !'po4a'hide' .B configfile +This file contains the definition of the time budgets for the users. +.PP +. +.SH USER AUTHENTICATION +.PP +This helper needs to know the identity of the user to associate a time +budget with this user. Currently only proxy_auth is able to deliver +this information. The following paragraph gives a short overview of a +very basic setup using "basic_ncsa_auth". More different options and a +complete and detailed explanation can be found in the Squid user +manual. +.PP +Start by setting up a file containing usernames and the corresponding +passwords. Use the htpasswd program coming with Apache to enter the +data and repeat this step for all users. +.PP +.if !'po4a'hide' .RS +root# +.if !'po4a'hide' .B htpasswd /etc/squid/passwd john +.if !'po4a'hide' .br +New password: +.if !'po4a'hide' .B johnssecret +.if !'po4a'hide' .br +Re-type new password: +.if !'po4a'hide' .B johnssecret +.if !'po4a'hide' .br +Adding password for user john +.if !'po4a'hide' .RE +.PP +Edit squid.conf to define a basic authentication program called +"basic_ncsa_auth", which authenticates users with the above password +file. Define a ACL using that program and deny access to web pages for +unautenticated users. +.PP +.if !'po4a'hide' .RS +# +.if !'po4a'hide' .br +# Define program and password file for auth. +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .B auth_param basic program /usr/libexec/basic_ncsa_auth /etc/squid/passwd +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +# Define ACL +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .B acl authenticated_users proxy_auth REQUIRED +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +# Deny access for unauthenticated users +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .B http_access deny !authenticated_users +.if !'po4a'hide' .br +.if !'po4a'hide' .RE +.PP +After restarting Squid it should allow access only for authenticated +users with the configured names and passwords. All other users will be +rejected. +. +.SH DEFINING TIME QUOTAS +.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 username +followed by a time budget and a corresponding time period separated by +"/". Here is an example file: +.PP +.if !'po4a'hide' .RS +# username 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. +. +.SH CONFIGURATION +.PP +This helper is also configured in squid.conf where you first declare the helper, then define a ACL which then decides when to allow or deny. Enter the following text +.if !'po4a'hide' .B after +the user authentication. +.PP +.if !'po4a'hide' .RS +# +.if !'po4a'hide' .br +# Define program and quota file +.if !'po4a'hide' .br +# +.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 -b /var/run/squid/time_quota.db /etc/squid/time_quota +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +# Define ACL for time_quota helper +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .br +.if !'po4a'hide' .B acl time_quota external time_quota +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +# Deny access if quota exceeded +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .B http_access deny !time_quota +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +# If quota exceeds, tell user +.if !'po4a'hide' .br +# +.if !'po4a'hide' .br +.if !'po4a'hide' .B deny_info TIME_QUOTA_EXCEEDED time_quota +.if !'po4a'hide' .RE +.PP +After restarting Squid it should allow access only for authenticated +users as long as they have time budget left. If the buget is exceeded +or an invalid username or password is given, the user will be prompted +for a correct username and password having time quota left. +.PP +For Unix machines it should be possible for the parents to +authenticate using ident so that they always have access to the +internet even without supplying user names and passwords. If this +"falls through", then the above helper would be called. This could be +done by something like "http_access allow ident" with a correct ident +configuration. This is out of scope of this manual page. +. +.SH LOGGING +. +Whenever you hit a problem, then start the helper with +.if !'po4a'hide' .B -l logfile +(e.g. "-l /tmp/time_quota.log") and look into that log file to find +any problem. +. +.SH LIMITATIONS +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 HTTP proxies like +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. +Sometimes one can sucessfully use the internet browser on those devices +but often online games and other stuff fails. +.PP +A more general control to internet access could be a captive portal +(like pfSense or ChilliSpot) or maybe a 802.11X solution. But the +latter is often not supported by mobile devices. +. +.SH IMPLEMENTATION +The helper is called once a minute and asked if the current user is +allowed to access squid. The helper will reduce the remaining time +budget of this user and return "OK" if there is budget left. Otherwise +it will return "ERR". +.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 5 minutes, the current request will be considered as a +new request and the time budget will not be decreased. If the time is +less than 5 minutes, than both request 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 make arbitrary breaks +during internet access without decreasing the 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 its 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 staid how long in the WWW. +.PP +. +.SH AUTHOR +This program and documentation was written by +.if !'po4a'hide' .I Dr. Tilmann Bubeck +. +.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' +. +.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 +.PP +Report ideas for new improvements to the +.I Squid Developers mailing list +.if !'po4a'hide' +. +.SH SEE ALSO +.if !'po4a'hide' .BR squid "(8), " +.if !'po4a'hide' .BR basic_ncsa_auth "(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 -r -u -N trunk/helpers/external_acl/time_quota/ext_time_quota_acl.cc trunk-time_quota/helpers/external_acl/time_quota/ext_time_quota_acl.cc --- trunk/helpers/external_acl/time_quota/ext_time_quota_acl.cc 1970-01-01 01:00:00.000000000 +0100 +++ trunk-time_quota/helpers/external_acl/time_quota/ext_time_quota_acl.cc 2011-04-04 21:19:49.000000000 +0200 @@ -0,0 +1,398 @@ +/* + * ext_time_quota_acl: Squid external acl helper for quota on usage. + * + * Copyright (C) 2011 Dr. Tilmann Bubeck + * + * 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 +#include +#include +#include +#include +#include +#if HAVE_UNISTD_H +#include +#endif +#include +#include +#if HAVE_GETOPT_H +#include +#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 +#elif HAVE_DB_H +#include +#endif + +char *db_path = NULL; +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" + +/** 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 void open_log(const char *logfilename) { + logfile = fopen(logfilename, "a"); + if ( logfile == NULL ) { + perror(logfilename); + logfile = stderr; + } +} + +static void log(const char *format, ...) +{ + va_list args; + time_t now = time(NULL); + + fprintf(logfile, "%ld: ", now); + va_start (args, format); + vfprintf (logfile, format, args); + va_end (args); + fflush(logfile); +} + +static void init_db(void) +{ + log("opening time quota database \"%s\".\n", db_path); + db = dbopen(db_path, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL); + if (!db) { + log("FATAL: %s: Failed to open time_quota db '%s'\n", program_name, db_path); + fprintf(stderr, "FATAL: %s: Failed to open time_quota db '%s'\n", program_name, db_path); + exit(1); + } +} + +static void shutdown_db(void) +{ + db->close(db); +} + +int session_is_active = 0; + +static void writeTime(const char *user_key, const char *sub_key, time_t t) +{ + char keybuffer[1024]; + DBT key, data; + + if ( strlen(user_key) + strlen(sub_key) + 1 + 1 > sizeof(keybuffer) ) { + fprintf(stderr, "ERROR: %s: key too long (%s,%s)\n", + program_name, 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("writeTime(\"%s\", %d)\n", keybuffer, t); + } +} + +static time_t readTime(const char *user_key, const char *sub_key) +{ + char keybuffer[1024]; + DBT key, data; + time_t t = 0; + + if ( strlen(user_key) + 1 + strlen(sub_key) + 1 > sizeof(keybuffer) ) { + fprintf(stderr, "ERROR: %s: key too long (%s,%s)\n", + program_name, 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)) { + fprintf(stderr, "ERROR: %s: CORRUPTED DATABASE (%s)\n", program_name, keybuffer); + } else { + memcpy(&t, data.data, sizeof(t)); + } + } + log("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("Wrong time unit \"%c\". Only \"m\", \"h\", \"d\", or \"w\" allowed.\n"); + 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[1024]; /* 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("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("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("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; + + log("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("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("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("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("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("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); + if ( timeBudgetCurrent > 0 ) { + log("OK for user \"%s\".\n", user_key); + SEND_OK(""); + } else { + log("ERR for user \"%s\".\n", user_key); + SEND_ERR("Time budget exceeded."); + } + + db->sync(db, 0); +} + +static void usage(void) +{ + log("Wrong usage. Please reconfigure in squid.conf.\n"); + + fprintf(stderr, "Usage: %s [-l logfile] [-b dbpath]\n", program_name); + fprintf(stderr, " -l logfile log messages to logfile\n"); + fprintf(stderr, " -b dbpath Path where persistent session database will be kept\n"); +} + +int main(int argc, char **argv) +{ + char request[HELPER_INPUT_BUFFER]; + int opt; + + program_name = argv[0]; + + while ((opt = getopt(argc, argv, "l:b:h?")) != -1) { + switch (opt) { + case 'l': + open_log(optarg); + break; + case 'b': + db_path = optarg; + break; + case 'h': + case '?': + usage(); + exit(0); + break; + } + } + + log("Starting %s\n", __FILE__); + setbuf(stdout, NULL); + + init_db(); + + if ( optind + 1 != argc ) { + usage(); + exit(1); + } else { + readConfig(argv[optind]); + } + + log("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("Ending %s\n", __FILE__); + shutdown_db(); + return 0; +} diff -r -u -N trunk/helpers/external_acl/time_quota/Makefile.am trunk-time_quota/helpers/external_acl/time_quota/Makefile.am --- trunk/helpers/external_acl/time_quota/Makefile.am 1970-01-01 01:00:00.000000000 +0100 +++ trunk-time_quota/helpers/external_acl/time_quota/Makefile.am 2011-03-19 17:29:11.000000000 +0100 @@ -0,0 +1,10 @@ +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 + +LDADD = \ + $(COMPAT_LIB) \ + $(LIB_DB)