diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2007-10-26 09:32:17 +0200 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2007-10-26 09:32:17 +0200 |
commit | eae71c5c2d5175931a95ae1ee3b66de05e01e9f0 (patch) | |
tree | 512ec91ad79833705e697168cf7e7eed8459c0b9 /nslcd/ldap-nss.c | |
parent | 12db792ffe944f2b22a49d87a93337bf03804f31 (diff) |
revert myldap changes in revisions 443 and 444
git-svn-id: http://arthurdejong.org/svn/nss-pam-ldapd/nss-ldapd-0.4.1@457 ef36b2f9-881f-0410-afb5-c4e39611909c
Diffstat (limited to 'nslcd/ldap-nss.c')
-rw-r--r-- | nslcd/ldap-nss.c | 604 |
1 files changed, 87 insertions, 517 deletions
diff --git a/nslcd/ldap-nss.c b/nslcd/ldap-nss.c index 63c70b0..44023f5 100644 --- a/nslcd/ldap-nss.c +++ b/nslcd/ldap-nss.c @@ -35,11 +35,13 @@ #include <stdio.h> #include <signal.h> #include <fcntl.h> -#include <time.h> #include <sys/time.h> #include <sys/socket.h> #include <sys/param.h> #include <errno.h> +#ifdef HAVE_SYS_UN_H +#include <sys/un.h> +#endif #include <netinet/in.h> #include <ldap.h> #ifdef HAVE_LDAP_SSL_H @@ -51,6 +53,14 @@ #ifdef HAVE_GSSSASL_H #include <gsssasl.h> #endif +#ifdef HAVE_MALLOC_H +#include <malloc.h> +#endif +#if defined(HAVE_THREAD_H) +#include <thread.h> +#elif defined(HAVE_PTHREAD_H) +#include <pthread.h> +#endif /* Try to handle systems with both SASL libraries installed */ #if defined(HAVE_SASL_SASL_H) && defined(HAVE_SASL_AUXPROP_REQUEST) #include <sasl/sasl.h> @@ -63,26 +73,20 @@ #include <gssapi/gssapi.h> #include <gssapi/gssapi_krb5.h> #endif -#include <ctype.h> #include "ldap-nss.h" -#include "myldap.h" #include "pagectrl.h" #include "common.h" #include "log.h" #include "cfg.h" #include "attmap.h" #include "compat/ldap.h" -#include "common/dict.h" /* * LS_INIT only used for enumeration contexts */ #define LS_INIT(state) do { state.ls_type = LS_TYPE_INDEX; state.ls_retry = 0; state.ls_info.ls_index = -1; } while (0) -/* the maximum number of searches per session */ -#define MAX_SEARCHES_IN_SESSION 4 - /* * convenient wrapper around pointer into global config list, and a * connection to an LDAP server. @@ -97,160 +101,11 @@ struct ldap_session int is_connected; /* index into ldc_uris: currently connected LDAP uri */ int ls_current_uri; - /* a list of searches registered with this session */ - struct myldap_search *searches[MAX_SEARCHES_IN_SESSION]; }; -/* A search description set as returned by myldap_search(). */ -struct myldap_search -{ - /* reference to the session */ - MYLDAP_SESSION *session; - /* the context used for this set, reused for later calls */ - struct ent_context context; - /* the parameters descibing the search */ - const char *base; - int scope; - const char *filter; - char **attrs; - /* a pointer to the current result entry, used for - freeing resource allocated with that entry */ - MYLDAP_ENTRY *entry; -}; - -/* A single entry from the LDAP database as returned by - myldap_get_entry(). */ -struct myldap_entry -{ - /* reference to the search to be used to get parameters - (e.g. LDAP connection) for other calls */ - MYLDAP_SEARCH *search; - /* reference to the LDAP message describing the result */ - LDAPMessage *msg; - /* the DN */ - const char *dn; - /* a cached version of the exploded rdn */ - char **exploded_rdn; - /* a cache of attribute to value list */ - DICT *attributevalues; -}; - -static MYLDAP_ENTRY *myldap_entry_new(MYLDAP_SEARCH *search,LDAPMessage *msg) -{ - MYLDAP_ENTRY *entry; - /* Note: as an alternative we could embed the myldap_entry into the - myldap_search struct to save on malloc() and free() calls. */ - /* allocate new entry */ - entry=(MYLDAP_ENTRY *)malloc(sizeof(struct myldap_entry)); - if (entry==NULL) - { - log_log(LOG_CRIT,"myldap_entry_new(): malloc() failed to allocate memory"); - exit(EXIT_FAILURE); - } - /* fill in fields */ - entry->search=search; - entry->msg=msg; - entry->dn=NULL; - entry->exploded_rdn=NULL; - entry->attributevalues=dict_new(); - /* return the fresh entry */ - return entry; -} - -static void myldap_entry_free(MYLDAP_ENTRY *entry) -{ - char **values; - /* free the DN */ - if (entry->dn!=NULL) - ldap_memfree((char *)entry->dn); - /* free the exploded RDN */ - if (entry->exploded_rdn!=NULL) - ldap_value_free(entry->exploded_rdn); - /* free all attribute values */ - dict_values_first(entry->attributevalues); - while ((values=(char **)dict_values_next(entry->attributevalues))!=NULL) - ldap_value_free(values); - dict_free(entry->attributevalues); - /* we don't need the result anymore, ditch it. */ - ldap_msgfree(entry->search->context.ec_res); - entry->search->context.ec_res=NULL; - /* apparently entry->msg does not need to be freed */ - entry->msg=NULL; - /* free the actual memory for the struct */ - free(entry); -} - -static MYLDAP_SEARCH *myldap_search_new( - MYLDAP_SESSION *session, - const char *base,int scope,const char *filter,const char **attrs) -{ - char *buffer; - MYLDAP_SEARCH *search; - int i; - size_t sz; - /* figure out size for new memory block to allocate - this has the advantage that we can free the whole lot with one call */ - sz=sizeof(struct myldap_search); - sz+=strlen(base)+1+strlen(filter)+1; - for (i=0;attrs[i]!=NULL;i++) - sz+=strlen(attrs[i])+1; - sz+=(i+1)*sizeof(char *); - /* allocate new results memory region */ - buffer=(char *)malloc(sz); - if (buffer==NULL) - { - log_log(LOG_CRIT,"myldap_search_new(): malloc() failed to allocate memory"); - exit(EXIT_FAILURE); - } - /* initialize struct */ - search=(MYLDAP_SEARCH *)(buffer); - buffer+=sizeof(struct myldap_search); - /* save pointer to session */ - search->session=session; - /* initialize array of attributes */ - search->attrs=(char **)buffer; - buffer+=(i+1)*sizeof(char *); - /* copy base */ - strcpy(buffer,base); - search->base=buffer; - buffer+=strlen(base)+1; - /* just plainly store scope */ - search->scope=scope; - /* copy filter */ - strcpy(buffer,filter); - search->filter=buffer; - buffer+=strlen(filter)+1; - /* copy attributes themselves */ - for (i=0;attrs[i]!=NULL;i++) - { - strcpy(buffer,attrs[i]); - search->attrs[i]=buffer; - buffer+=strlen(attrs[i])+1; - } - search->attrs[i]=NULL; - /* initialize context */ - _nss_ldap_ent_context_init(&(search->context),session); - /* clear result entry */ - search->entry=NULL; - /* return the new search struct */ - return search; -} - -static void myldap_search_free(MYLDAP_SEARCH *search) -{ - /* free any search entries */ - if (search->entry!=NULL) - myldap_entry_free(search->entry); - /* free the context */ - _nss_ldap_ent_context_cleanup(&(search->context)); - /* free the storage we allocated */ - free(search); -} - -static MYLDAP_SESSION *myldap_session_new(void) +MYLDAP_SESSION *myldap_create_session(void) { MYLDAP_SESSION *session; - int i; /* allocate memory for the session storage */ session=(struct ldap_session *)malloc(sizeof(struct ldap_session)); if (session==NULL) @@ -263,8 +118,6 @@ static MYLDAP_SESSION *myldap_session_new(void) session->ls_timestamp=0; session->is_connected=0; session->ls_current_uri=0; - for (i=0;i<MAX_SEARCHES_IN_SESSION;i++) - session->searches[i]=NULL; /* return the new session */ return session; } @@ -734,7 +587,7 @@ void _nss_ldap_ent_context_cleanup(struct ent_context *context) */ static int do_search_sync( MYLDAP_SESSION *session,const char *base,int scope, - const char *filter,char **attrs,int sizelimit, + const char *filter,const char **attrs,int sizelimit, LDAPMessage **res) { int rc; @@ -759,7 +612,7 @@ static int do_search_sync( */ static int do_search_async( MYLDAP_SESSION *session,const char *base,int scope, - const char *filter,char **attrs,int sizelimit,int *msgid) + const char *filter,const char **attrs,int sizelimit,int *msgid) { int rc; LDAPControl *serverCtrls[2]; @@ -791,7 +644,7 @@ static int do_search_async( */ static enum nss_status do_with_reconnect( MYLDAP_SESSION *session,const char *base,int scope, - const char *filter,char **attrs,int sizelimit, + const char *filter,const char **attrs,int sizelimit, LDAPMessage **res,int *msgid) { int rc=LDAP_UNAVAILABLE, tries=0, backoff=0; @@ -891,6 +744,65 @@ static enum nss_status do_with_reconnect( } /* + * Parse, fetching reuslts from chain instead of server. + */ +static enum nss_status do_parse_sync( + struct ent_context *context,void *result, + char *buffer,size_t buflen,parser_t parser) +{ + enum nss_status parseStat=NSS_STATUS_NOTFOUND; + LDAPMessage *e=NULL; + + log_log(LOG_DEBUG,"==> do_parse_sync"); + + /* + * if ec_state.ls_info.ls_index is non-zero, then we don't collect another + * entry off the LDAP chain, and instead refeed the existing result to + * the parser. Once the parser has finished with it, it will return + * NSS_STATUS_NOTFOUND and reset the index to -1, at which point we'll retrieve + * another entry. + */ + do + { + if ((context->ec_state.ls_retry==0) && + ( (context->ec_state.ls_type==LS_TYPE_KEY) || + (context->ec_state.ls_info.ls_index==-1) )) + { + if (e==NULL) + e=ldap_first_entry(context->session->ls_conn,context->ec_res); + else + e=ldap_next_entry(context->session->ls_conn,e); + } + + if (e==NULL) + { + /* Could not get a result; bail */ + parseStat=NSS_STATUS_NOTFOUND; + break; + } + + /* + * We have an entry; now, try to parse it. + * + * If we do not parse the entry because of a schema + * violation, the parser should return NSS_STATUS_NOTFOUND. + * We'll keep on trying subsequent entries until we + * find one which is parseable, or exhaust avialable + * entries, whichever is first. + */ + parseStat=parser(context->session,e,&(context->ec_state),result,buffer,buflen); + + /* hold onto the state if we're out of memory XXX */ + context->ec_state.ls_retry=(parseStat==NSS_STATUS_TRYAGAIN)&&(buffer!=NULL); + } + while (parseStat==NSS_STATUS_NOTFOUND); + + log_log(LOG_DEBUG,"<== do_parse_sync"); + + return parseStat; +} + +/* * Simple wrapper around ldap_get_values(). Requires that * session is already established. */ @@ -918,339 +830,6 @@ static int nss2nslcd(enum nss_status code) } } -MYLDAP_SESSION *myldap_create_session(void) -{ - return myldap_session_new(); -} - -void myldap_session_cleanup(MYLDAP_SESSION *session) -{ - int i; - /* go over all searches in the session */ - for (i=0;i<MAX_SEARCHES_IN_SESSION;i++) - { - if (session->searches[i]!=NULL) - { - myldap_search_close(session->searches[i]); - session->searches[i]=NULL; - } - } -} - -MYLDAP_SEARCH *myldap_search( - MYLDAP_SESSION *session, - const char *base,int scope,const char *filter,const char **attrs) -{ - MYLDAP_SEARCH *search; - int msgid; - int i; - /* check parameters */ - if ((session==NULL)||(base==NULL)||(filter==NULL)||(attrs==NULL)) - { - log_log(LOG_ERR,"myldap_search(): invalid parameter passed"); - errno=EINVAL; - return NULL; - } - /* log the call */ - log_log(LOG_DEBUG,"myldap_search(base=\"%s\", filter=\"%s\")", - base,filter); - /* allocate a new search entry */ - search=myldap_search_new(session,base,scope,filter,attrs); - /* set up a new search */ - if (do_with_reconnect(search->session,search->base, - search->scope,search->filter,search->attrs, - LDAP_NO_LIMIT,NULL,&msgid)!=NSS_STATUS_SUCCESS) - { - myldap_search_free(search); - return NULL; - } - search->context.ec_msgid=msgid; - /* find a place in the session where we can register our search */ - for (i=0;(session->searches[i]!=NULL)&&(i<MAX_SEARCHES_IN_SESSION);i++) - ; - if (i>=MAX_SEARCHES_IN_SESSION) - { - log_log(LOG_ERR,"too many searches registered with session (max %d)",MAX_SEARCHES_IN_SESSION); - myldap_search_free(search); - return NULL; - } - /* regsiter search with the session so we can free it later on */ - session->searches[i]=search; - return search; -} - -void myldap_search_close(MYLDAP_SEARCH *search) -{ - int i; - if ((search==NULL)||(search->session==NULL)) - return; - /* find the reference to this search in the session */ - for (i=0;i<MAX_SEARCHES_IN_SESSION;i++) - { - if (search->session->searches[i]==search) - search->session->searches[i]=NULL; - } - /* free this search */ - myldap_search_free(search); -} - -MYLDAP_ENTRY *myldap_get_entry(MYLDAP_SEARCH *search) -{ - enum nss_status stat=NSS_STATUS_SUCCESS; - int msgid; - int rc; - /* check parameters */ - if ((search==NULL)||(search->session==NULL)||(search->session->ls_conn==NULL)) - { - log_log(LOG_ERR,"myldap_get_entry(): invalid search entry passed"); - errno=EINVAL; - return NULL; - } - /* if we have an existing result entry, free it */ - if (search->entry!=NULL) - { - myldap_entry_free(search->entry); - search->entry=NULL; - } - /* try to parse results until we have a final error or ok */ - while (1) - { - /* get an entry from the LDAP server, the result - is stored in context->ec_res */ - stat=do_result_async(&(search->context)); - /* we we have an entry construct a search entry from it */ - if (stat==NSS_STATUS_SUCCESS) - { - /* we have a normal entry, return it */ - search->entry=myldap_entry_new(search,search->context.ec_res); - return search->entry; - } - else if ( (stat==NSS_STATUS_NOTFOUND) && - (search->context.ec_cookie!=NULL) && - (search->context.ec_cookie->bv_len!=0) ) - { - /* we are using paged results, try the next page */ - LDAPControl *serverctrls[2]={ NULL, NULL }; - rc=ldap_create_page_control(search->session->ls_conn, - nslcd_cfg->ldc_pagesize, - search->context.ec_cookie,0,&serverctrls[0]); - if (rc!=LDAP_SUCCESS) - { - log_log(LOG_WARNING,"myldap_get_entry(): ldap_create_page_control() failed: %s", - ldap_err2string(rc)); - /* FIXME: figure out if we need to free something */ - return NULL; - } - rc=ldap_search_ext(search->session->ls_conn, - search->base,search->scope,search->filter, - search->attrs,0,serverctrls,NULL,LDAP_NO_LIMIT, - LDAP_NO_LIMIT,&msgid); - ldap_control_free(serverctrls[0]); - if (msgid<0) - { - log_log(LOG_WARNING,"myldap_get_entry(): ldap_search_ext() failed: %s", - ldap_err2string(rc)); - /* FIXME: figure out if we need to free something */ - return NULL; - } - search->context.ec_msgid=msgid; - /* we continue with another pass */ - } - else - { - log_log(LOG_DEBUG,"myldap_get_entry(): do_result_async() returned error code"); - /* there was another problem, bail out */ - return NULL; - } - } -} - -/* - * Get the DN from the entry. This function only returns NULL (and sets - * errno) if an incorrect entry is passed. If the DN value cannot be - * retreived "unknown" is returned instead. - */ -const char *myldap_get_dn(MYLDAP_ENTRY *entry) -{ - int rc; - /* check parameters */ - if ((entry==NULL)||(entry->search==NULL)||(entry->search->session==NULL)|| - (entry->search->session->ls_conn==NULL)||(entry->msg==NULL)) - { - log_log(LOG_ERR,"myldap_get_dn(): invalid result entry passed"); - errno=EINVAL; - return NULL; - } - /* if we don't have it yet, retreive it */ - if (entry->dn==NULL) - { - entry->dn=ldap_get_dn(entry->search->session->ls_conn,entry->msg); - if (entry->dn==NULL) - { - if (ldap_get_option(entry->search->session->ls_conn,LDAP_OPT_ERROR_NUMBER,&rc)!=LDAP_SUCCESS) - rc=LDAP_UNAVAILABLE; - log_log(LOG_WARNING,"ldap_get_dn() returned NULL: %s",ldap_err2string(rc)); - } - } - /* if we still don't have it, return unknown */ - if (entry->dn==NULL) - return "unknown"; - /* return it */ - return entry->dn; -} - -/* Simple wrapper around ldap_get_values(). */ -const char **myldap_get_values(MYLDAP_ENTRY *entry,const char *attr) -{ - char **values; - int rc; - /* check parameters */ - if ((entry==NULL)||(entry->search==NULL)||(entry->search->session==NULL)|| - (entry->search->session->ls_conn==NULL)||(entry->msg==NULL)) - { - log_log(LOG_ERR,"myldap_get_values(): invalid result entry passed"); - errno=EINVAL; - return NULL; - } - else if (attr==NULL) - { - log_log(LOG_ERR,"myldap_get_values(): invalid attribute name passed"); - errno=EINVAL; - return NULL; - } - /* get the values from the cache */ - values=(char **)dict_get(entry->attributevalues,attr); - if (values==NULL) - { - /* cache miss, get from LDAP */ - values=ldap_get_values(entry->search->session->ls_conn,entry->msg,attr); - if (values==NULL) - { - if (ldap_get_option(entry->search->session->ls_conn,LDAP_OPT_ERROR_NUMBER,&rc)!=LDAP_SUCCESS) - rc=LDAP_UNAVAILABLE; - log_log(LOG_WARNING,"myldap_get_values(): ldap_get_values() returned NULL: %s",ldap_err2string(rc)); - } - /* store values entry so we can free it later on */ - if (values!=NULL) - dict_put(entry->attributevalues,attr,values); - } - return (const char **)values; -} - -/* return the number of elements in the array returned by - by myldap_get_values() */ -int myldap_count_values(const char **vals) -{ - int i; - if (vals==NULL) - return 0; - for (i=0;vals[i]!=NULL;i++) - /* nothing here */; - return i; -} - -/* Go over the entries in exploded_rdn and see if any start with - the requested attribute. Return a reference to the value part of - the DN (does not modify exploded_rdn). */ -static const char *find_rdn_value(char **exploded_rdn,const char *attr) -{ - int i,j; - int l; - if (exploded_rdn==NULL) - return NULL; - /* go over all RDNs */ - l=strlen(attr); - for (i=0;exploded_rdn[i]!=NULL;i++) - { - /* check that RDN starts with attr */ - if (strncasecmp(exploded_rdn[i],attr,l)!=0) - continue; - /* skip spaces */ - for (j=l;isspace(exploded_rdn[i][j]);j++) - /* nothing here */; - /* ensure that we found an equals sign now */ - if (exploded_rdn[i][j]!='=') - j++; - /* skip more spaces */ - for (j++;isspace(exploded_rdn[i][j]);j++) - /* nothing here */; - /* ensure that we're not at the end of the string */ - if (exploded_rdn[i][j]=='\0') - continue; - /* we found our value */ - return exploded_rdn[i]+j; - } - /* fail */ - return NULL; -} - -const char *myldap_get_rdn_value(MYLDAP_ENTRY *entry,const char *attr) -{ - const char *dn; - char **exploded_dn; - /* check parameters */ - if ((entry==NULL)||(entry->search==NULL)||(entry->search->session==NULL)|| - (entry->search->session->ls_conn==NULL)||(entry->msg==NULL)) - { - log_log(LOG_ERR,"myldap_get_rdn_value(): invalid result entry passed"); - errno=EINVAL; - return NULL; - } - else if (attr==NULL) - { - log_log(LOG_ERR,"myldap_get_rdn_value(): invalid attribute name passed"); - errno=EINVAL; - return NULL; - } - /* check if entry contains exploded_rdn */ - if (entry->exploded_rdn==NULL) - { - /* check if we have a DN */ - dn=myldap_get_dn(entry); - if (dn==NULL) - return NULL; - /* explode dn into { "uid=test", "ou=people", ..., NULL } */ - exploded_dn=ldap_explode_dn(dn,0); - if ((exploded_dn==NULL)||(exploded_dn[0]==NULL)) - { - log_log(LOG_WARNING,"myldap_get_rdn_value(): ldap_explode_dn(%s) returned NULL: %s", - dn,strerror(errno)); - return NULL; - } - /* explode rdn (first part of exploded_dn), - e.g. "cn=Test User+uid=testusr" into - { "cn=Test User", "uid=testusr", NULL } */ - entry->exploded_rdn=ldap_explode_rdn(exploded_dn[0],0); - ldap_value_free(exploded_dn); - } - /* find rnd value */ - return find_rdn_value(entry->exploded_rdn,attr); -} - -int myldap_has_objectclass(MYLDAP_ENTRY *entry,const char *objectclass) -{ - const char **values; - int i; - if ((entry==NULL)||(objectclass==NULL)) - { - log_log(LOG_ERR,"myldap_has_objectclass(): invalid argument passed"); - errno=EINVAL; - return 0; - } - values=myldap_get_values(entry,"objectClass"); - if (values==NULL) - { - log_log(LOG_ERR,"myldap_has_objectclass(): myldap_get_values() returned NULL"); - return 0; - } - for (i=0;values[i]!=NULL;i++) - { - if (strcasecmp(values[i],objectclass)==0) - return -1; - } - return 0; -} - /* * Internal entry point for enumeration routines. * This should really use the asynchronous LDAP search API to avoid @@ -1269,7 +848,7 @@ int _nss_ldap_getent( if (context->ec_msgid<0) { /* set up a new search */ - stat=do_with_reconnect(context->session,base,scope,filter,(char **)attrs,LDAP_NO_LIMIT,NULL,&msgid); + stat=do_with_reconnect(context->session,base,scope,filter,attrs,LDAP_NO_LIMIT,NULL,&msgid); if (stat != NSS_STATUS_SUCCESS) return nss2nslcd(stat); context->ec_msgid=msgid; @@ -1360,34 +939,25 @@ int _nss_ldap_getbyname(MYLDAP_SESSION *session,void *result, char *buffer, size const char *base,int scope,const char *filter,const char **attrs, parser_t parser) { - MYLDAP_SEARCH *search; - MYLDAP_ENTRY *entry; enum nss_status stat = NSS_STATUS_NOTFOUND; - /* do the search */ - search=myldap_search(session,base,scope,filter,attrs); - if (search==NULL) - return NSLCD_RESULT_UNAVAIL; + struct ent_context context; + log_log(LOG_DEBUG,"_nss_ldap_getbyname(base=\"%s\", filter=\"%s\"",base,filter); + _nss_ldap_ent_context_init(&context,session); + /* synchronous search */ + stat=do_with_reconnect(context.session,base,scope,filter,attrs,1,&context.ec_res,NULL); + if (stat!=NSS_STATUS_SUCCESS) + return nss2nslcd(stat); /* * we pass this along for the benefit of the services parser, * which uses it to figure out which protocol we really wanted. * we only pass the second argument along, as that's what we need * in services. */ - search->context.ec_state.ls_type=LS_TYPE_KEY; - search->context.ec_state.ls_info.ls_key=NULL /*was: args->la_arg2.la_string*/; - do - { - entry = myldap_get_entry(search); - if (entry!=NULL) - { - stat=parser(session,entry->msg,&(search->context.ec_state),result,buffer,buflen); - /* hold onto the state if we're out of memory XXX */ - search->context.ec_state.ls_retry=(stat==NSS_STATUS_TRYAGAIN)&&(buffer!=NULL); - } - } - while ((stat==NSS_STATUS_NOTFOUND)&&(entry!=NULL)); - /* clean up this search */ - myldap_search_close(search); + LS_INIT(context.ec_state); + context.ec_state.ls_type=LS_TYPE_KEY; + context.ec_state.ls_info.ls_key=NULL /*was: args->la_arg2.la_string*/; + stat=do_parse_sync(&context,result,buffer,buflen,parser); + _nss_ldap_ent_context_cleanup(&context); return nss2nslcd(stat); } |