summaryrefslogtreecommitdiff
path: root/daemon/bd.c
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/bd.c')
-rw-r--r--daemon/bd.c679
1 files changed, 364 insertions, 315 deletions
diff --git a/daemon/bd.c b/daemon/bd.c
index e11a56b..eb0bec0 100644
--- a/daemon/bd.c
+++ b/daemon/bd.c
@@ -29,6 +29,7 @@
#include "basic.h"
#include "md5.h"
#include "bd.h"
+#include "stringx.h"
static unsigned char g_digest_secret[DIGEST_SECRET_LEN];
@@ -38,193 +39,277 @@ static unsigned char g_digest_secret[DIGEST_SECRET_LEN];
* Defaults and Constants
*/
-#define BASIC_ESTABLISHED (void*)1
+enum {
+ RECORD_TYPE_BASIC,
+ RECORD_TYPE_DIGEST
+};
/* Kept by the us for validating the client */
-typedef struct digest_record
-{
- unsigned char nonce[DIGEST_NONCE_LEN];
- unsigned char userhash[MD5_LEN];
- unsigned char ha1[MD5_LEN];
- unsigned int nc;
-}
-digest_record_t;
+typedef struct bd_record {
+ int type;
+
+ /* Used for Digest */
+ unsigned char nonce[DIGEST_NONCE_LEN];
+ unsigned char userhash[MD5_LEN];
+ unsigned char ha1[MD5_LEN];
+ unsigned int nc;
+
+ /* Used for both */
+ char **groups;
+} bd_record_t;
/* -------------------------------------------------------------------------------
* Internal Functions
*/
-static void free_hash_object(void* arg, void* val)
+static void
+free_hash_object (void *arg, void *val)
{
- if(val && val != BASIC_ESTABLISHED)
- free(val);
+ bd_record_t *rec = val;
+ if (!rec)
+ return;
+ str_array_free (rec->groups);
+ free (rec);
}
-static digest_record_t* get_cached_digest(bd_context_t* ctx, ha_context_t* c,
- unsigned char* nonce)
+static int
+have_cached_basic (bd_context_t* ctx, unsigned char* key)
{
- digest_record_t* rec;
+ bd_record_t *rec;
- ASSERT(ctx && c && nonce);
+ ASSERT (ctx && key);
- if(c->cache_max == 0)
- return NULL;
+ ha_lock(NULL);
- ha_lock(NULL);
-
- rec = (digest_record_t*)hsh_get(ctx->cache, nonce);
+ rec = (bd_record_t*)hsh_get (ctx->cache, key);
+ if (rec && rec->type != RECORD_TYPE_BASIC)
+ rec = NULL;
+ else
+ hsh_touch (ctx->cache, key);
- /* Just in case it's a basic :) */
- if(rec && rec != BASIC_ESTABLISHED)
- hsh_rem(ctx->cache, nonce);
-
- ha_unlock(NULL);
+ ha_unlock(NULL);
- ASSERT(!rec || memcmp(nonce, rec->nonce, DIGEST_NONCE_LEN) == 0);
- return rec;
+ return rec != NULL;
}
-static int have_cached_basic(bd_context_t* ctx, unsigned char* key)
+static int
+add_cached_basic (bd_context_t *ctx, unsigned char *key, char **groups)
{
- int ret = 0;
+ bd_record_t *rec;
+ int r;
- ASSERT(ctx && key);
+ ASSERT (ctx && key);
- ha_lock(NULL);
+ rec = (bd_record_t*)malloc (sizeof (*rec));
+ if (!rec) {
+ str_array_free (groups);
+ ha_memerr (NULL);
+ return HA_CRITERROR;
+ }
- ret = (hsh_get(ctx->cache, key) == BASIC_ESTABLISHED);
+ memset (rec, 0, sizeof (*rec));
+ rec->type = RECORD_TYPE_BASIC;
+ rec->groups = groups;
- ha_unlock(NULL);
+ ha_lock (NULL);
- return ret;
-}
+ while (hsh_count (ctx->cache) >= ctx->cache_max)
+ hsh_bump (ctx->cache);
-static int save_cached_digest(bd_context_t* ctx, ha_context_t* c,
- digest_record_t* rec)
-{
- int r;
+ r = hsh_set (ctx->cache, key, rec);
- ASSERT(ctx && rec);
+ ha_unlock (NULL);
- if(c->cache_max == 0)
- {
- free_hash_object(NULL, rec);
- return HA_FALSE;
- }
+ if (!r) {
+ free_hash_object (NULL, rec);
+ ha_memerr (NULL);
+ return HA_CRITERROR;
+ }
- ha_lock(NULL);
+ return HA_OK;
+}
- while(hsh_count(ctx->cache) >= c->cache_max)
- hsh_bump(ctx->cache);
+static int
+prepare_digest_from_cached (bd_context_t *ctx, digest_context_t *dg,
+ ha_request_t *rq, unsigned char *nonce)
+{
+ bd_record_t *rec;
+ int ret;
+
+ ASSERT (dg && rq && nonce);
+
+ ha_lock (NULL);
+
+ rec = (bd_record_t*)hsh_get (ctx->cache, nonce);
+ if (rec && rec->type == RECORD_TYPE_DIGEST) {
+ ASSERT (memcmp (nonce, rec->nonce, DIGEST_NONCE_LEN) == 0);
+ memcpy (dg->server_userhash, rec->userhash, sizeof (dg->server_userhash));
+ memcpy (dg->server_ha1, rec->ha1, sizeof (dg->server_ha1));
+ dg->server_nc = ++(rec->nc);
+ hsh_touch (ctx->cache, nonce);
+ ret = HA_OK;
+ } else {
+ memset (dg->server_userhash, 0, sizeof (dg->server_userhash));
+ memset (dg->server_ha1, 0, sizeof (dg->server_ha1));
+ dg->server_nc = 1;
+ ret = HA_FALSE;
+ }
+
+ ha_unlock (NULL);
+
+ dg->server_uri = rq->req_args[AUTH_ARG_URI];
+ dg->server_method = rq->req_args[AUTH_ARG_METHOD];
+
+ return ret;
+}
- r = hsh_set(ctx->cache, rec->nonce, rec);
+static int
+add_digest_rec (bd_context_t *ctx, unsigned char *nonce,
+ const char *user, digest_context_t *dg, char **groups)
+{
+ bd_record_t* rec = (bd_record_t*)malloc (sizeof (*rec));
+ int r;
- ha_unlock(NULL);
+ ASSERT (nonce && user);
- if(!r)
- {
+ if(!rec) {
+ str_array_free (groups);
ha_memerr(NULL);
- return HA_CRITERROR;
- }
+ return HA_CRITERROR;
+ }
- return HA_OK;
-}
+ memset (rec, 0, sizeof (*rec));
+ rec->type = RECORD_TYPE_DIGEST;
+ memcpy (rec->nonce, nonce, DIGEST_NONCE_LEN);
+ memcpy (rec->ha1, dg->server_ha1, MD5_LEN);
-static int add_cached_basic(bd_context_t* ctx, ha_context_t* c,
- unsigned char* key)
-{
- int r;
+ /* We take ownership of groups */
+ rec->groups = groups;
+ rec->nc = dg->server_nc;
- ASSERT(ctx && c && key);
+ md5_string (rec->userhash, user);
- if(c->cache_max == 0)
- return HA_FALSE;
+ ha_lock (NULL);
- ha_lock(NULL);
+ while (hsh_count (ctx->cache) >= ctx->cache_max)
+ hsh_bump (ctx->cache);
- while(hsh_count(ctx->cache) >= c->cache_max)
- hsh_bump(ctx->cache);
+ r = hsh_set (ctx->cache, nonce, rec);
- r = hsh_set(ctx->cache, key, BASIC_ESTABLISHED);
+ ha_unlock (NULL);
- ha_unlock(NULL);
-
- if(!r)
- {
- ha_memerr(NULL);
- return HA_CRITERROR;
- }
+ if (!r) {
+ free_hash_object (NULL, rec);
+ ha_memerr (NULL);
+ return HA_CRITERROR;
+ }
- return HA_OK;
+ return HA_OK;
}
-digest_record_t* make_digest_rec(unsigned char* nonce, const char* user)
+static int
+include_group_headers (bd_context_t *ctx, ha_request_t *rq, unsigned char *key)
{
- digest_record_t* rec = (digest_record_t*)malloc(sizeof(*rec));
-
- ASSERT(nonce && user);
-
- if(!rec)
- {
- ha_memerr(NULL);
- return NULL;
- }
-
- memset(rec, 0, sizeof(*rec));
- memcpy(rec->nonce, nonce, DIGEST_NONCE_LEN);
-
- md5_string(rec->userhash, user);
- return rec;
+ bd_record_t *rec;
+ int have, all = 0;
+ char *header;
+ char **ug;
+ char **rg;
+
+ if (!rq->requested_groups || !rq->requested_groups[0])
+ return HA_OK;
+
+ ha_lock (NULL);
+
+ /* This starts a new block to join */
+ ha_bufcpy (rq->buf, "");
+
+ rec = hsh_get (ctx->cache, key);
+ if (rec) {
+ for (ug = rec->groups; ug && *ug; ++ug) {
+ have = all;
+ for (rg = rq->requested_groups; rg && *rg && !have; ++rg) {
+ if (strcasecmp (*rg, "*") == 0) {
+ have = all = 1;
+ break;
+ } else if (strcasecmp (*rg, *ug) == 0) {
+ have = 1;
+ break;
+ }
+ }
+
+ if (have) {
+ ha_bufjoin (rq->buf);
+ ha_bufcpy (rq->buf, "\"");
+ ha_bufjoin (rq->buf);
+ digest_escape (rq->buf, *ug);
+ ha_bufjoin (rq->buf);
+ ha_bufcpy (rq->buf, "\" ");
+ }
+ }
+ }
+
+ ha_unlock (NULL);
+
+ header = ha_bufdata (rq->buf);
+ if (!header)
+ return HA_CRITERROR;
+
+ ha_addheader (rq, "X-HttpAuth-Groups", header);
+ return HA_OK;
}
-static int do_basic_response(ha_request_t* rq, bd_context_t* ctx, const char* header)
+static int
+do_basic_response (ha_request_t* rq, bd_context_t* ctx, const char* header)
{
- basic_header_t basic;
- int ret = HA_FALSE;
+ basic_header_t basic;
+ char **groups = NULL;
+ int ret = HA_FALSE;
- ASSERT(header && rq);
+ ASSERT (header && rq);
- if((ret = basic_parse(header, rq->buf, &basic)) < 0)
- return ret;
+ if ((ret = basic_parse (header, rq->buf, &basic)) < 0)
+ return ret;
- /* Past this point we don't return directly */
+ /* Past this point we don't return directly */
- /* Check and see if this connection is in the cache */
- if(have_cached_basic(ctx, basic.key))
- {
- ha_messagex(rq, LOG_NOTICE, "validated basic user against cache: %s",
- basic.user);
- RETURN(HA_OK);
- }
+ /* Check and see if this connection is in the cache */
+ if (have_cached_basic (ctx, basic.key)) {
+ ha_messagex (rq, LOG_NOTICE, "validated basic user against cache: %s",
+ basic.user);
+ RETURN (HA_OK);
+ }
- /* If we have a user name and password */
- if(!basic.user || !basic.user[0] ||
- !basic.password || !basic.password[0])
- {
- ha_messagex(rq, LOG_NOTICE, "no valid basic auth info");
- RETURN(HA_FALSE);
- }
+ /* If we have a user name and password */
+ if(!basic.user || !basic.user[0] ||
+ !basic.password || !basic.password[0]) {
+ ha_messagex(rq, LOG_NOTICE, "no valid basic auth info");
+ RETURN(HA_FALSE);
+ }
- ASSERT(ctx->f_validate_basic);
- ret = ctx->f_validate_basic(rq, basic.user, basic.password);
+ ASSERT (ctx->f_validate_basic);
+ ret = ctx->f_validate_basic (rq, basic.user, basic.password, &groups);
-finally:
+ /*
+ * We put this connection into the successful connections.
+ * This takes ownership of groups.
+ */
+ if (ret == HA_OK)
+ ret = add_cached_basic (ctx, basic.key, groups);
- if(ret == HA_OK)
- {
- rq->resp_code = HA_SERVER_OK;
- rq->resp_detail = basic.user;
+finally:
- /* We put this connection into the successful connections */
- ret = add_cached_basic(ctx, rq->context, basic.key);
- }
+ if (ret == HA_OK) {
+ rq->resp_code = HA_SERVER_OK;
+ rq->resp_detail = basic.user;
+ include_group_headers (ctx, rq, basic.key);
+ }
- return ret;
+ return ret;
}
static int do_digest_challenge(ha_request_t* rq, bd_context_t* ctx, int stale)
{
- unsigned char nonce[DIGEST_NONCE_LEN];
const char* nonce_str;
const char* header;
@@ -264,208 +349,172 @@ static int do_digest_challenge(ha_request_t* rq, bd_context_t* ctx, int stale)
static int do_digest_response(ha_request_t* rq, bd_context_t* ctx, const char* header)
{
- unsigned char nonce[DIGEST_NONCE_LEN];
- digest_context_t dg;
- digest_record_t* rec = NULL;
- const char* t;
- time_t expiry;
- int ret = HA_FALSE;
- int stale = 0;
- int r;
+ unsigned char nonce[DIGEST_NONCE_LEN];
+ digest_context_t dg;
+ const char *t;
+ char **groups = NULL;
+ time_t expiry;
+ int ret = HA_FALSE;
+ int stale = 0;
+ int r, cached;
- ASSERT(ctx && header && rq);
+ ASSERT (ctx && header && rq);
- /* We use this below to send a default response */
- rq->resp_code = -1;
+ /* We use this below to send a default response */
+ rq->resp_code = -1;
- if((r = digest_parse(header, rq->buf, &(dg.client))) < 0)
- return r;
+ if ((r = digest_parse (header, rq->buf, &(dg.client))) < 0)
+ return r;
- if(!dg.client.username)
- {
- ha_messagex(rq, LOG_WARNING, "digest response contains no user name");
- RETURN(HA_FALSE);
- }
+ if (!dg.client.username) {
+ ha_messagex(rq, LOG_WARNING, "digest response contains no user name");
+ RETURN(HA_FALSE);
+ }
#ifdef _DEBUG
- if(rq->context->digest_debugnonce)
- {
- if(dg.client.nonce && strcmp(dg.client.nonce, rq->context->digest_debugnonce) != 0)
- {
- ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce");
- RETURN(HA_FALSE);
- }
-
- /* Do a rough hash into the real nonce, for use as a key */
- md5_string(nonce, rq->context->digest_debugnonce);
-
- /* Debug nonce's never expire */
- expiry = time(NULL);
- }
- else
+ if (rq->context->digest_debugnonce) {
+ if (dg.client.nonce && strcmp (dg.client.nonce, rq->context->digest_debugnonce) != 0) {
+ ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce");
+ RETURN(HA_FALSE);
+ }
+
+ /* Do a rough hash into the real nonce, for use as a key */
+ md5_string (nonce, rq->context->digest_debugnonce);
+
+ /* Debug nonce's never expire */
+ expiry = time (NULL);
+ } else
#endif
- {
- /* Parse out the nonce from that header */
- memset(nonce, 0, DIGEST_NONCE_LEN);
-
- if(dg.client.nonce)
- {
- size_t len = DIGEST_NONCE_LEN;
- void* d = ha_bufdechex(rq->buf, dg.client.nonce, &len);
-
- if(d && len == DIGEST_NONCE_LEN)
- memcpy(nonce, d, DIGEST_NONCE_LEN);
- }
-
- r = digest_checknonce(nonce, g_digest_secret, &expiry);
- if(r != HA_OK)
- {
- if(r == HA_FALSE)
- ha_messagex(rq, LOG_WARNING, "digest response contains invalid nonce");
-
- RETURN(r);
- }
-
- /* Check to see if we're stale */
- if((expiry + rq->context->cache_timeout) <= time(NULL))
- {
- ha_messagex(rq, LOG_INFO, "nonce expired, sending stale challenge: %s",
- dg.client.username);
-
- stale = 1;
- RETURN(HA_FALSE);
- }
- }
-
- /* See if we have this one from before. */
- rec = get_cached_digest(ctx, rq->context, nonce);
-
- /*
- * Fill in the required fields.
- */
- dg.server_nc = rec ? ++(rec->nc) : 0; /* Note bumping up nc */
- dg.server_uri = rq->req_args[AUTH_ARG_URI];
- dg.server_method = rq->req_args[AUTH_ARG_METHOD];
-
- /* Check the majority of the fields */
- ret = digest_pre_check(&dg, rq->context, rq->buf);
-
- if(ret != HA_OK)
- {
- if(ret == HA_BADREQ)
- {
- ret = HA_FALSE;
- rq->resp_code = HA_SERVER_BADREQ;
- }
-
- RETURN(ret);
- }
-
- /*
- * If this is the first instance then we pass off to our derived
- * handler for validation and completion of the ha1. This completes
- * the authentication, and leaves us the ha1 caching.
- */
- if(!rec)
- {
- ha_messagex(rq, LOG_INFO, "no record in cache, creating one: %s", dg.client.username);
-
- /*
- * If we're valid but don't have a record in the
- * cache then complete the record properly.
- */
-
- rec = make_digest_rec(nonce, dg.client.username);
- if(!rec)
- RETURN(HA_CRITERROR);
-
-
- ASSERT(ctx->f_validate_digest);
- r = ctx->f_validate_digest(rq, dg.client.username, &dg);
- if(r != HA_OK)
- RETURN(r);
-
- /* Save away pertinent information when successful*/
- memcpy(rec->ha1, dg.ha1, MD5_LEN);
- }
-
- /* We had a record so ... */
- else
- {
- /* Bump up the ncount */
- rec->nc++;
-
- /* Check the user name */
- if(md5_strcmp(rec->userhash, dg.client.username) != 0)
- {
- ha_messagex(NULL, LOG_ERR, "digest response contains invalid username");
- RETURN(HA_FALSE);
- }
-
- /* And do the validation ourselves */
- memcpy(dg.ha1, rec->ha1, MD5_LEN);
-
- ret = digest_complete_check(&dg, rq->context, rq->buf);
-
- if(ret != HA_OK)
- {
- if(ret == HA_BADREQ)
- {
- ret = HA_FALSE;
- rq->resp_code = HA_SERVER_BADREQ;
- }
-
- if(ret == HA_FALSE)
- ha_messagex(NULL, LOG_WARNING, "digest re-authentication failed for user: %s",
- dg.client.username);
-
- RETURN(ret);
- }
- }
-
-
- rq->resp_code = HA_SERVER_OK;
- rq->resp_detail = dg.client.username;
-
- /* Figure out if we need a new nonce */
- if((expiry + (rq->context->cache_timeout -
- (rq->context->cache_timeout / 8))) < time(NULL))
- {
- ha_messagex(rq, LOG_INFO, "nonce almost expired, creating new one: %s",
- dg.client.username);
-
- digest_makenonce(nonce, g_digest_secret, NULL);
- stale = 1;
- }
-
- t = digest_respond(&dg, rq->buf, stale ? nonce : NULL);
- if(!t)
- RETURN(HA_CRITERROR);
-
- if(t[0])
- ha_addheader(rq, "Authentication-Info", t);
-
- ha_messagex(rq, LOG_NOTICE, "validated digest user: %s", dg.client.username);
-
- /* Put the connection into the cache */
- if((ret = save_cached_digest(ctx, rq->context, rec)) == HA_OK)
- rec = NULL;
+ {
+ /* Parse out the nonce from that header */
+ memset (nonce, 0, DIGEST_NONCE_LEN);
+
+ if (dg.client.nonce) {
+ size_t len = DIGEST_NONCE_LEN;
+ void* d = ha_bufdechex (rq->buf, dg.client.nonce, &len);
+
+ if (d && len == DIGEST_NONCE_LEN)
+ memcpy (nonce, d, DIGEST_NONCE_LEN);
+ }
+
+ r = digest_checknonce (nonce, g_digest_secret, &expiry);
+ if (r != HA_OK) {
+ if (r == HA_FALSE)
+ ha_messagex (rq, LOG_WARNING, "digest response contains invalid nonce");
+ stale = 1;
+ RETURN (r);
+ }
+
+ /* Check to see if we're stale */
+ if ((expiry + rq->context->cache_timeout) <= time (NULL)) {
+ ha_messagex(rq, LOG_INFO, "nonce expired, sending stale challenge: %s",
+ dg.client.username);
+
+ stale = 1;
+ RETURN (HA_FALSE);
+ }
+ }
+
+ /*
+ * Fill in all the required fields from any cached response,
+ * and check the user name if cached. Otherwise initializes
+ * to default values.
+ */
+ cached = (prepare_digest_from_cached (ctx, &dg, rq, nonce) == HA_OK);
+
+ /* Check the majority of the fields */
+ ret = digest_pre_check (&dg, rq->context, rq->buf);
+ if (ret != HA_OK) {
+ if (ret == HA_BADREQ) {
+ ret = HA_FALSE;
+ rq->resp_code = HA_SERVER_BADREQ;
+ }
+
+ RETURN (ret);
+ }
+
+ /*
+ * If this is the first instance then we pass off to our derived
+ * handler for validation and completion of the ha1. This completes
+ * the authentication, and leaves us the ha1 caching.
+ */
+ if (!cached) {
+ ha_messagex (rq, LOG_INFO, "no record in cache, creating one: %s",
+ dg.client.username);
+
+ /*
+ * If we're valid but don't have a record in the
+ * cache then complete the record properly.
+ */
+
+ ASSERT (ctx->f_validate_digest);
+ r = ctx->f_validate_digest (rq, dg.client.username, &dg, &groups);
+ if (r != HA_OK)
+ RETURN (r);
+
+ /* Add the digest record to the cache */
+ r = add_digest_rec (ctx, nonce, dg.client.username, &dg, groups);
+ if (r != HA_OK)
+ RETURN (r);
+
+ /* We had a record so ... */
+ } else {
+
+ /* Check the user name */
+ if (md5_strcmp (dg.server_userhash, dg.client.username) != 0) {
+ ha_messagex(NULL, LOG_ERR, "digest response contains invalid username");
+ RETURN(HA_FALSE);
+ }
+
+ /* And do the validation ourselves */
+ ret = digest_complete_check (&dg, rq->context, rq->buf);
+
+ if (ret != HA_OK) {
+ if (ret == HA_BADREQ) {
+ ret = HA_FALSE;
+ rq->resp_code = HA_SERVER_BADREQ;
+ }
+
+ if (ret == HA_FALSE)
+ ha_messagex (NULL, LOG_WARNING, "digest re-authentication failed for user: %s",
+ dg.client.username);
+
+ RETURN (ret);
+ }
+ }
+
+ rq->resp_code = HA_SERVER_OK;
+ rq->resp_detail = dg.client.username;
+ include_group_headers (ctx, rq, nonce);
+
+ /* Figure out if we need a new nonce */
+ if ((expiry + (rq->context->cache_timeout -
+ (rq->context->cache_timeout / 8))) < time (NULL)) {
+ ha_messagex (rq, LOG_INFO, "nonce almost expired, creating new one: %s",
+ dg.client.username);
+
+ digest_makenonce (nonce, g_digest_secret, NULL);
+ stale = 1;
+ }
+
+ t = digest_respond (&dg, rq->buf, stale ? nonce : NULL);
+ if (!t)
+ RETURN (HA_CRITERROR);
+
+ if (t[0])
+ ha_addheader(rq, "Authentication-Info", t);
+
+ ha_messagex(rq, LOG_NOTICE, "validated digest user: %s", dg.client.username);
finally:
- /* If the record wasn't stored away then free it */
- if(rec)
- free(rec);
-
- /* If nobody above responded then challenge the client again */
- if(ret == HA_FALSE && rq->resp_code == -1)
- return do_digest_challenge(rq, ctx, stale);
+ /* If nobody above responded then challenge the client again */
+ if (ret == HA_FALSE && rq->resp_code == -1)
+ return do_digest_challenge (rq, ctx, stale);
- return ret;
+ return ret;
}
-
-
const char* bd_substitute(const ha_request_t* rq, const char* user, const char* str)
{
bd_context_t* ctx = (bd_context_t*)rq->context->ctx_data;
@@ -559,6 +608,7 @@ int bd_init(ha_context_t* context)
htc.f_freeval = free_hash_object;
htc.arg = NULL;
hsh_set_table_calls(ctx->cache, &htc);
+ ctx->cache_max = context->cache_max;
}
return HA_OK;
@@ -567,7 +617,6 @@ int bd_init(ha_context_t* context)
void bd_destroy(ha_context_t* context)
{
bd_context_t* ctx;
- int i;
if(!context)
return;