Hi,
This is the first version of the traffic shaping patch for cherokee.
It's not yet finished but the main part is already working. To enable
the shaping one can use the 'Limit' directive on the config file either
on global scope or Server scope. Eg.
Limit 100
The yacc grammar doesn't yet understand the unit. Today it defaults to
kilobytes. On the future it'll be possible to specify megabytes and
gigabytes (Eg. 1000M or 100G)
The algorithm used to control the bandwidth was a simple FIFO. I think
it fits best cherokee among other complex algorithms.
The patch has been tested on Linux (FC4) and OpenBSD (3.7).
Feel free to give any suggestions, ideas, criticism, etc.
Thanks.
Diego Giagio
Index: cherokee/virtual_server.c
===================================================================
--- cherokee/virtual_server.c (revision 50)
+++ cherokee/virtual_server.c (working copy)
@@ -86,6 +86,11 @@
ret = cherokee_dirs_table_new (&vsrv->userdir_dirs);
if (unlikely(ret < ret_ok)) return ret;
+ /* Bandwidth throttler
+ */
+ cherokee_throttler_new (&vsrv->throttler);
+ if (unlikely(ret < ret_ok)) return ret;
+
/* Return the object
*/
*vserver = vsrv;
@@ -137,6 +142,13 @@
cherokee_buffer_free (vserver->name);
cherokee_buffer_free (vserver->root);
+ /* Destroy the bandwidth throttler
+ */
+ if (vserver->throttler != NULL) {
+ cherokee_throttler_free (vserver->throttler);
+ vserver->throttler = NULL;
+ }
+
if (vserver->logger != NULL) {
cherokee_logger_free (vserver->logger);
vserver->logger = NULL;
Index: cherokee/virtual_server.h
===================================================================
--- cherokee/virtual_server.h (revision 50)
+++ cherokee/virtual_server.h (working copy)
@@ -43,6 +43,7 @@
#include "exts_table.h"
#include "dirs_table_entry.h"
#include "logger.h"
+#include "throttler.h"
typedef struct {
struct list_head list_entry;
@@ -71,6 +72,8 @@
#endif
} data;
+ cherokee_throttler_t *throttler; /* Bandwidth throttler */
+
char *server_cert;
char *server_key;
char *ca_cert;
@@ -90,10 +93,10 @@
} cherokee_virtual_server_t;
-#define VSERVER(v) ((cherokee_virtual_server_t *)(v))
-#define VSERVER_LOGGER(v) (LOGGER(VSERVER(v)->logger))
+#define VSERVER(v) ((cherokee_virtual_server_t *)(v))
+#define VSERVER_LOGGER(v) (LOGGER(VSERVER(v)->logger))
+#define VSERVER_THROTTLER(v) (THROTTLER(VSERVER(v)->throttler))
-
ret_t cherokee_virtual_server_new (cherokee_virtual_server_t **vserver);
ret_t cherokee_virtual_server_free (cherokee_virtual_server_t *vserver);
ret_t cherokee_virtual_server_clean (cherokee_virtual_server_t *vserver);
Index: cherokee/throttler.c
===================================================================
--- cherokee/throttler.c (revision 0)
+++ cherokee/throttler.c (revision 0)
@@ -0,0 +1,299 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+/* Cherokee
+ *
+ * Authors:
+ * Alvaro Lopez Ortega <[EMAIL PROTECTED]>
+ *
+ * Copyright (C) 2001, 2002, 2003, 2004, 2005 Alvaro Lopez Ortega
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public
+ * License as published by the Free Software Foundation.
+ *
+ * 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-1307
+ * USA
+ */
+
+#include "common-internal.h"
+#include "throttler.h"
+#include "connection.h"
+
+#include <time.h>
+#include <sys/time.h>
+
+#ifndef timersub
+#define timersub(tvp, uvp, vvp) \
+ do { \
+ (vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec; \
+ (vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec; \
+ if ((vvp)->tv_usec < 0) { \
+ (vvp)->tv_sec--; \
+ (vvp)->tv_usec += 1000000; \
+ } \
+ } while (0)
+#endif
+
+#ifndef timerclear
+#define timerclear(tvp) (tvp)->tv_sec = (tvp)->tv_usec = 0
+#endif
+
+#ifndef CLOCKS_PER_SEC
+#define CLOCKS_PER_SEC CLK_TCK
+#endif
+
+#define ENTRY_MAX_AGE 1 /* 1 second */
+
+struct conn_entry_t {
+ struct list_head list;
+ cherokee_connection_t *conn;
+ time_t queue_time;
+};
+
+
+static ret_t
+conn_entry_new (struct conn_entry_t **entry, cherokee_connection_t *conn)
+{
+ CHEROKEE_NEW_TYPE(n, struct conn_entry_t);
+
+ n->conn = conn;
+ n->queue_time = 0;
+
+ INIT_LIST_HEAD(&n->list);
+
+ *entry = n;
+
+ return ret_ok;
+}
+
+static ret_t
+conn_entry_free (struct conn_entry_t *entry)
+{
+ free (entry);
+ return ret_ok;
+}
+
+ret_t
+cherokee_throttler_new (cherokee_throttler_t **throttler)
+{
+ CHEROKEE_NEW_STRUCT(n, throttler);
+
+ timerclear (&n->curr_time);
+ timerclear (&n->last_time);
+
+ n->bytes = 0;
+ n->limit = 0;
+
+ /* Initialize the connection queue
+ */
+ INIT_LIST_HEAD(&n->conn_queue);
+
+ /* Initialize the mutex
+ */
+ CHEROKEE_MUTEX_INIT(&n->mutex, NULL);
+
+ *throttler = n;
+ return ret_ok;
+}
+
+ret_t
+cherokee_throttler_free (cherokee_throttler_t *throttler)
+{
+ struct conn_entry_t *entry;
+ struct list_head *pos;
+ struct list_head *pos_tmp;
+
+ /* Free remaining entries in the queue
+ */
+ list_for_each_safe (pos, pos_tmp, &throttler->conn_queue) {
+ entry = list_entry (pos, struct conn_entry_t, list);
+ list_del (&entry->list);
+ free (entry);
+ }
+
+ free (throttler);
+ return ret_ok;
+}
+
+static unsigned long inline
+tv2ms (struct timeval tv)
+{
+ unsigned long ms;
+ ms = tv.tv_sec * 1000;
+ ms += tv.tv_usec / 1000;
+
+ return ms == 0 ? 1 : ms;
+}
+
+static void
+conn_queue_put (cherokee_throttler_t *throttler, cherokee_connection_t *conn)
+{
+ struct list_head *pos;
+ struct conn_entry_t *entry;
+ int do_queue = true;
+
+ /* Check if this connection is already queued
+ */
+ list_for_each (pos, &throttler->conn_queue) {
+ entry = list_entry (pos, struct conn_entry_t, list);
+ if (entry->conn == conn) {
+ /* Don't requeue
+ */
+ do_queue = false;
+ break;
+ }
+ }
+
+ if (do_queue) {
+ conn_entry_new (&entry, conn);
+ time (&entry->queue_time);
+
+ list_add_tail (&entry->list, &throttler->conn_queue);
+ }
+}
+
+static void
+conn_queue_del (cherokee_throttler_t *throttler, struct conn_entry_t *entry)
+{
+ /* Delete entry
+ */
+ list_del (&entry->list);
+}
+
+static struct conn_entry_t *
+conn_queue_peek (cherokee_throttler_t *throttler)
+{
+ struct conn_entry_t *entry;
+ struct list_head *pos;
+ time_t now;
+
+ time (&now);
+
+ peek_loop:
+ entry = NULL;
+
+ if (!list_empty (&throttler->conn_queue)) {
+ pos = throttler->conn_queue.next;
+ entry = list_entry (pos, struct conn_entry_t, list);
+
+ if ((now - entry->queue_time) > ENTRY_MAX_AGE) {
+ /* Remove old entry
+ */
+ conn_queue_del (throttler, entry);
+ conn_entry_free (entry);
+ goto peek_loop;
+ }
+ }
+
+ return entry;
+}
+
+ret_t
+cherokee_throttler_check (cherokee_throttler_t *throttler,
+ cherokee_connection_t *conn)
+{
+ struct conn_entry_t *entry;
+ struct timeval tv;
+ unsigned long curr_speed;
+
+ if (throttler == NULL || throttler->limit == 0)
+ return ret_ok;
+
+ CHEROKEE_MUTEX_LOCK (&throttler->mutex);
+
+ /* Retrieve current time
+ */
+ gettimeofday (&throttler->curr_time, NULL);
+
+ /* Calculate the diff between the current and last time
+ */
+ timersub (&throttler->curr_time, &throttler->last_time, &tv);
+
+ /* Calculate current speed in kb/s
+ */
+ curr_speed = throttler->bytes / ((tv2ms (tv) * 1024) / 1000);
+
+ /* Check if the current speed has exceeded the limit
+ */
+ if (curr_speed > throttler->limit) {
+ conn_queue_put (throttler, conn);
+ CHEROKEE_MUTEX_UNLOCK (&throttler->mutex);
+ return ret_eagain;
+ }
+
+ /* Get the current entry from the queue
+ */
+ entry = conn_queue_peek (throttler);
+ if (entry != NULL) {
+ /* Check if it's the connection we should give bandwidth
+ */
+ if (entry->conn != conn) {
+ CHEROKEE_MUTEX_UNLOCK (&throttler->mutex);
+ return ret_eagain;
+ }
+
+ /* Remove this connection from the queue
+ */
+ conn_queue_del (throttler, entry);
+
+ /* Free this entry
+ */
+ conn_entry_free (entry);
+ }
+
+ /* Update time and byte counter
+ */
+ if (throttler->curr_time.tv_sec > throttler->last_time.tv_sec) {
+ throttler->last_time = throttler->curr_time;
+ throttler->bytes = 0;
+ }
+
+ CHEROKEE_MUTEX_UNLOCK (&throttler->mutex);
+ return ret_ok;
+}
+
+ret_t
+cherokee_throttler_wait (void)
+{
+ static long nsec = 0;
+ struct timespec ts;
+
+ /* Calculate the clock resolution in nanoseconds
+ */
+ if (nsec == 0) {
+ clock_t start, current;
+ double msec;
+
+ start = clock ();
+ while ((current = clock()) == start) ;
+
+ msec = ((current - start)/(double)CLOCKS_PER_SEC) * 1000;
+ nsec = msec * 1000000;
+ }
+
+ /* Sleep a bit
+ */
+ ts.tv_sec = 0;
+ ts.tv_nsec = nsec;
+ nanosleep (&ts, NULL);
+}
+
+ret_t
+cherokee_throttler_update (cherokee_throttler_t *throttler, size_t n)
+{
+ if (throttler == NULL || throttler->limit == 0)
+ return ret_ok;
+
+ CHEROKEE_MUTEX_LOCK (&throttler->mutex);
+ throttler->bytes += n;
+ CHEROKEE_MUTEX_UNLOCK (&throttler->mutex);
+
+ return ret_ok;
+}
Index: cherokee/throttler.h
===================================================================
--- cherokee/throttler.h (revision 0)
+++ cherokee/throttler.h (revision 0)
@@ -0,0 +1,67 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+/* Cherokee
+ *
+ * Authors:
+ * Alvaro Lopez Ortega <[EMAIL PROTECTED]>
+ *
+ * Copyright (C) 2001, 2002, 2003, 2004, 2005 Alvaro Lopez Ortega
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public
+ * License as published by the Free Software Foundation.
+ *
+ * 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-1307
+ * USA
+ */
+
+#if !defined (CHEROKEE_INSIDE_CHEROKEE_H) && !defined (CHEROKEE_COMPILATION)
+# error "Only <cherokee/cherokee.h> can be included directly, this file may disappear or change contents."
+#endif
+
+#ifndef CHEROKEE_THROTTLER_H
+#define CHEROKEE_THROTTLER_H
+
+#include <cherokee/common.h>
+#include "connection.h"
+#include "list.h"
+
+#include <sys/time.h>
+
+CHEROKEE_BEGIN_DECLS
+
+typedef struct {
+ struct timeval curr_time;
+ struct timeval last_time;
+
+ unsigned long bytes;
+ unsigned long limit;
+
+ struct list_head conn_queue;
+
+#ifdef HAVE_PTHREAD
+ pthread_mutex_t mutex;
+#endif
+
+} cherokee_throttler_t;
+
+
+#define THROTTLER(t) ((cherokee_throttler_t *)(t))
+
+ret_t cherokee_throttler_new (cherokee_throttler_t **throttler);
+ret_t cherokee_throttler_free (cherokee_throttler_t *throttler);
+
+ret_t cherokee_throttler_check (cherokee_throttler_t *throttler, cherokee_connection_t *conn);
+ret_t cherokee_throttler_wait (void);
+ret_t cherokee_throttler_update (cherokee_throttler_t *throttler, size_t n);
+
+CHEROKEE_END_DECLS
+
+#endif /* CHEROKEE_CONNECTION_H */
Index: cherokee/read_config_grammar.y
===================================================================
--- cherokee/read_config_grammar.y (revision 46)
+++ cherokee/read_config_grammar.y (working copy)
@@ -281,7 +281,7 @@
%token T_SERVER T_USERDIR T_PIDFILE T_LISTEN T_SERVER_TOKENS T_ENCODER T_ALLOW T_IO_CACHE T_DIRECTORYINDEX
%token T_ICONS T_AUTH T_NAME T_METHOD T_PASSWDFILE T_SSL_CA_LIST_FILE T_FROM T_SOCKET T_LOG_FLUSH_INTERVAL
%token T_INCLUDE T_PANIC_ACTION T_JUST_ABOUT T_LISTEN_QUEUE_SIZE T_SENDFILE T_MINSIZE T_MAXSIZE T_MAX_FDS
-%token T_SHOW T_CHROOT T_ONLY_SECURE T_MAX_CONNECTION_REUSE T_REWRITE T_POLL_METHOD T_EXTENSION T_IPV6 T_ENV
+%token T_SHOW T_CHROOT T_ONLY_SECURE T_MAX_CONNECTION_REUSE T_REWRITE T_POLL_METHOD T_EXTENSION T_IPV6 T_ENV T_LIMIT
%token <number> T_NUMBER T_PORT
%token <string> T_QSTRING T_FULLDIR T_ID T_HTTP_URL T_HTTPS_URL T_HOSTNAME T_IP T_DOMAIN_NAME T_ADDRESS_PORT
@@ -347,6 +347,7 @@
| ssl_key_file
| ssl_ca_list_file
| userdir
+ | limit
;
directory_options :
@@ -464,6 +465,18 @@
SRV(server)->port = $2;
};
+limit : T_LIMIT T_NUMBER
+{
+ cherokee_virtual_server_t *vsrv = auto_virtual_server;
+ long limit;
+
+ limit = $2;
+ if (limit < 0)
+ limit = 0;
+
+ VSERVER_THROTTLER(vsrv)->limit = limit;
+};
+
listen : T_LISTEN host_name
{
SRV(server)->listen_to = $2;
@@ -512,7 +525,6 @@
cherokee_buffer_add (vserver->root, root, root_len);
};
-
log : T_LOG T_ID
{
ret_t ret;
Index: cherokee/thread.c
===================================================================
--- cherokee/thread.c (revision 46)
+++ cherokee/thread.c (working copy)
@@ -418,11 +418,11 @@
off_t len;
list_t *i, *tmp;
cherokee_boolean_t process;
+ cherokee_boolean_t throttler_wait = false;
cherokee_connection_t *conn = NULL;
cherokee_server_t *srv = thd->server;
-
/* Process active connections
*/
list_for_each_safe (i, tmp, (list_t*)&thd->active_list) {
@@ -449,16 +449,30 @@
*/
process = ((conn->phase == phase_reading_header) &&
(!cherokee_buffer_is_empty (conn->incoming_header)));
-
+
/* Process the connection?
* 2.- Inspect the file descriptor
*/
if (process == false) {
int num;
+ /* First, check if this connection is being throttled
+ * and there's enough bandwidth to proceed
+ */
+ if (CONN_THROTTLER(conn) != NULL) {
+ ret = cherokee_throttler_check (CONN_THROTTLER(conn), conn);
+ if (ret != ret_ok) {
+ throttler_wait = true;
+ continue;
+ }
+ }
+
+ /* Then, check if the fd is ready
+ */
num = cherokee_fdpoll_check (thd->fdpoll,
SOCKET_FD(conn->socket),
SOCKET_STATUS(conn->socket));
+
switch (num) {
case -1:
purge_closed_connection(thd, conn);
@@ -469,7 +483,7 @@
process = true;
}
-
+
/* Process the connection?
* Finial.-
*/
@@ -667,6 +681,10 @@
*/
conn->logger_ref = CONN_VSRV(conn)->logger;
+ /* Setup the bandwidth throttler
+ */
+ conn->throttler = CONN_VSRV(conn)->throttler;
+
/* Is it already an error response?
*/
if (http_type_300(conn->error_code) ||
@@ -862,7 +880,6 @@
conn->phase = phase_send_headers;
case phase_send_headers:
-
/* Send headers to the client
*/
ret = cherokee_connection_send_header (conn);
@@ -997,6 +1014,10 @@
}
} /* list */
+ if (throttler_wait) {
+ cherokee_throttler_wait ();
+ }
+
return ret_ok;
}
@@ -1039,7 +1060,7 @@
cherokee_sockaddr_t new_sa;
cherokee_connection_t *new_conn;
-
+
/* Return if there're no new connections
*/
if (cherokee_fdpoll_check (thd->fdpoll, srv_socket, 0) == 0) {
Index: cherokee/handler_file.c
===================================================================
--- cherokee/handler_file.c (revision 50)
+++ cherokee/handler_file.c (working copy)
@@ -390,24 +390,26 @@
}
#endif
- /* Maybe use sendfile
- */
+ if (CONN_THROTTLER(conn)->limit == 0) {
+ /* Maybe use sendfile
+ */
#ifdef HAVE_SENDFILE
- n->using_sendfile = ((conn->mmaped == NULL) &&
- (conn->encoder == NULL) &&
- (n->info->st_size >= srv->sendfile.min) &&
- (n->info->st_size < srv->sendfile.max) &&
- (conn->socket->is_tls == non_TLS));
+ n->using_sendfile = ((conn->mmaped == NULL) &&
+ (conn->encoder == NULL) &&
+ (n->info->st_size >= srv->sendfile.min) &&
+ (n->info->st_size < srv->sendfile.max) &&
+ (conn->socket->is_tls == non_TLS));
# ifdef HAVE_SENDFILE_BROKEN
- n->using_sendfile = false;
+ n->using_sendfile = false;
# endif
- if (n->using_sendfile) {
- cherokee_connection_set_cork(conn, 1);
+ if (n->using_sendfile) {
+ cherokee_connection_set_cork(conn, 1);
+ }
+#endif
}
-#endif
-
+
return ret_ok;
}
Index: cherokee/Makefile.am
===================================================================
--- cherokee/Makefile.am (revision 46)
+++ cherokee/Makefile.am (working copy)
@@ -715,7 +715,9 @@
handler_error.c \
handler_error.h \
nonce.c \
-nonce.h
+nonce.h \
+throttler.h \
+throttler.c
libcherokee_config_la_SOURCES = \
admin_client.h \
Index: cherokee/read_config_scanner.l
===================================================================
--- cherokee/read_config_scanner.l (revision 46)
+++ cherokee/read_config_scanner.l (working copy)
@@ -112,6 +112,7 @@
"MaxConnectionReuse" { return T_MAX_CONNECTION_REUSE; }
"IOCache" { return T_IO_CACHE; }
"Env" { return T_ENV; }
+"Limit" { return T_LIMIT; }
"On" { yylval.number = 1; return T_NUMBER; }
"Off" { yylval.number = 0; return T_NUMBER; }
Index: cherokee/connection.c
===================================================================
--- cherokee/connection.c (revision 51)
+++ cherokee/connection.c (working copy)
@@ -117,6 +117,7 @@
n->tx_partial = 0;
n->traffic_next = 0;
n->validator = NULL;
+ n->throttler = NULL;
cherokee_buffer_new (&n->buffer);
cherokee_buffer_new (&n->header_buffer);
@@ -646,6 +647,7 @@
}
cherokee_connection_tx_add (cnt, re);
+ cherokee_throttler_update (CONN_THROTTLER(cnt), re);
cnt->mmaped_len -= re;
cnt->mmaped += re;
@@ -677,6 +679,14 @@
default:
RET_UNKNOWN(ret);
}
+
+ /* Add to the connection traffic counter
+ */
+ cherokee_connection_tx_add (cnt, re);
+
+ /* Update the bandwidth throttler
+ */
+ cherokee_throttler_update (CONN_THROTTLER(cnt), re);
/* If writev() has not sent all data
*/
@@ -697,10 +707,6 @@
return ret_eagain;
}
- /* Add to the connection traffic counter
- */
- cherokee_connection_tx_add (cnt, re);
-
return ret_ok;
}
@@ -732,6 +738,7 @@
switch (ret) {
case ret_ok:
cherokee_connection_rx_add (cnt, readed);
+ cherokee_throttler_update (CONN_THROTTLER(cnt), readed);
*len = readed;
return ret_ok;
@@ -804,6 +811,10 @@
*/
cherokee_connection_tx_add (cnt, sent);
+ /* Update the bandwidth throttler
+ */
+ cherokee_throttler_update (CONN_THROTTLER(cnt), sent);
+
/* Drop out the sent info
*/
if (sent == cnt->buffer->len) {
@@ -833,6 +844,10 @@
*/
cherokee_connection_tx_add (cnt, sent);
+ /* Update the bandwidth throttler
+ */
+ cherokee_throttler_update (CONN_THROTTLER(cnt), sent);
+
/* Drop out the sent info
*/
if (sent == cnt->buffer->len) {
Index: cherokee/connection-protected.h
===================================================================
--- cherokee/connection-protected.h (revision 46)
+++ cherokee/connection-protected.h (working copy)
@@ -60,6 +60,7 @@
#include "encoder.h"
#include "iocache.h"
#include "encoder_table.h"
+#include "throttler.h"
typedef enum {
phase_nothing,
@@ -157,6 +158,10 @@
size_t tx_partial; /* TX partial counter */
time_t traffic_next; /* Time to update traffic */
+ /* Bandwidth throttler
+ */
+ cherokee_throttler_t *throttler;
+
/* Post info
*/
cherokee_buffer_t *post;
@@ -175,13 +180,13 @@
cherokee_iocache_entry_t *io_entry_ref;
};
-#define CONN_SRV(c) (SRV(CONN(c)->server))
-#define CONN_HDR(c) (HDR(CONN(c)->header))
-#define CONN_SOCK(c) (SOCKET(CONN(c)->socket))
-#define CONN_VSRV(c) (VSERVER(CONN(c)->vserver))
-#define CONN_THREAD(c) (THREAD(CONN(c)->thread))
+#define CONN_SRV(c) (SRV(CONN(c)->server))
+#define CONN_HDR(c) (HDR(CONN(c)->header))
+#define CONN_SOCK(c) (SOCKET(CONN(c)->socket))
+#define CONN_VSRV(c) (VSERVER(CONN(c)->vserver))
+#define CONN_THREAD(c) (THREAD(CONN(c)->thread))
+#define CONN_THROTTLER(c) (THROTTLER(CONN(c)->throttler))
-
/* Basic functions
*/
ret_t cherokee_connection_new (cherokee_connection_t **cnt);
_______________________________________________
Cherokee mailing list
[email protected]
http://www.alobbs.com/cgi-bin/mailman/listinfo/cherokee