/************************************************************************
* IRC - Internet Relay Chat, src/s_misc.c
* Copyright (C) 1990 Jarkko Oikarinen and
* University of Oulu, Computing Center
*
* See file AUTHORS in IRC package for additional names of
* the programmers.
*
* 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/* $Id: s_misc.c,v 1.8 2006/01/01 00:55:22 tux316 Exp $ */
#include <sys/time.h>
#include "struct.h"
#include "common.h"
#include "sys.h"
#include "numeric.h"
#include "zlink.h"
#include "hooks.h"
#include "clones.h"
#include <sys/stat.h>
#include <fcntl.h>
#if !defined(ULTRIX) && !defined(SGI) && !defined(sequent) && \
!defined(__convex__)
#include <sys/param.h>
#endif
#if defined(AIX) || defined(SVR3) || \
((__GNU_LIBRARY__ == 6) && (__GLIBC__ >=2) && (__GLIBC_MINOR__ >= 2))
#include <time.h>
#endif
#include "h.h"
#include "fdlist.h"
extern float curSendK, curRecvK;
extern int server_was_split;
#ifdef ALWAYS_SEND_DURING_SPLIT
int currently_processing_netsplit = NO;
#endif
static void exit_one_client(aClient *, aClient *, aClient *, char *);
static char *months[] =
{
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"
};
static char *weekdays[] =
{
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"
};
/* stats stuff */
struct stats ircst, *ircstp = &ircst;
char *
date(time_t clock)
{
static char buf[80], plus;
struct tm *lt, *gm;
struct tm gmbuf;
int minswest;
if (!clock)
time(&clock);
gm = gmtime(&clock);
memcpy((char *) &gmbuf, (char *) gm, sizeof(gmbuf));
gm = &gmbuf;
lt = localtime(&clock);
if (lt->tm_yday == gm->tm_yday)
minswest = (gm->tm_hour - lt->tm_hour) * 60 + (gm->tm_min - lt->tm_min);
else if (lt->tm_yday > gm->tm_yday)
minswest = (gm->tm_hour - (lt->tm_hour + 24)) * 60;
else
minswest = ((gm->tm_hour + 24) - lt->tm_hour) * 60;
plus = (minswest > 0) ? '-' : '+';
if (minswest < 0)
minswest = -minswest;
ircsprintf(buf, "%s %s %d %04d -- %02d:%02d %c%02d:%02d",
weekdays[lt->tm_wday], months[lt->tm_mon], lt->tm_mday,
lt->tm_year + 1900, lt->tm_hour, lt->tm_min,
plus, minswest / 60, minswest % 60);
return buf;
}
char *
smalldate(time_t clock)
{
static char buf[MAX_DATE_STRING];
struct tm *lt, *gm;
struct tm gmbuf;
if (!clock)
time(&clock);
gm = gmtime(&clock);
memcpy((char *) &gmbuf, (char *) gm, sizeof(gmbuf));
gm = &gmbuf;
lt = localtime(&clock);
ircsprintf(buf, "%04d/%02d/%02d %02d.%02d", lt->tm_year + 1900,
lt->tm_mon + 1, lt->tm_mday, lt->tm_hour, lt->tm_min);
return buf;
}
/**
** myctime()
** This is like standard ctime()-function, but it zaps away
** the newline from the end of that string. Also, it takes
** the time value as parameter, instead of pointer to it.
** Note that it is necessary to copy the string to alternate
** buffer (who knows how ctime() implements it, maybe it statically
** has newline there and never 'refreshes' it -- zapping that
** might break things in other places...)
**
**/
char *
myctime(time_t value)
{
static char buf[28];
char *p;
strcpy(buf, ctime(&value));
if ((p = (char *) strchr(buf, '\n')) != NULL)
*p = '\0';
return buf;
}
/*
* * check_registered_user is used to cancel message, if the *
* originator is a server or not registered yet. In other * words,
* passing this test, *MUST* guarantee that the * sptr->user exists
* (not checked after this--let there * be coredumps to catch bugs...
* this is intentional --msa ;) *
*
* There is this nagging feeling... should this NOT_REGISTERED * error
* really be sent to remote users? This happening means * that remote
* servers have this user registered, although this * one has it not...
* Not really users fault... Perhaps this * error message should be
* restricted to local clients and some * other thing generated for
* remotes...
*/
inline int
check_registered_user(aClient *sptr)
{
if (!IsRegisteredUser(sptr))
{
sendto_one(sptr, err_str(ERR_NOTREGISTERED), me.name, "*");
return -1;
}
return 0;
}
/*
* * check_registered user cancels message, if 'x' is not * registered
* (e.g. we don't know yet whether a server * or user)
*/
inline int
check_registered(aClient *sptr)
{
if (!IsRegistered(sptr))
{
sendto_one(sptr, err_str(ERR_NOTREGISTERED), me.name, "*");
return -1;
}
return 0;
}
inline char *
get_listener_name(aListener *lptr)
{
static char nbuf[HOSTLEN * 2 + USERLEN + 5];
ircsprintf(nbuf, "%s[@%s.%d][%s]", me.name, BadPtr(lptr->vhost_string) ?
"0.0.0.0" : lptr->vhost_string, lptr->port,
BadPtr(lptr->allow_string) ? "*" : lptr->allow_string);
return nbuf;
}
/*
* * get_client_name * Return the name of the client for various
* tracking and * admin purposes. The main purpose of this
* function is to * return the "socket host" name of the client,
* if that * differs from the advertised name (other than case). *
* But, this can be used to any client structure. *
*
* Returns: * "name[user@ip#.port]" if 'showip' is true; *
* "name[sockethost]", if name and sockhost are different and *
* showip is false; else * "name". *
*
* NOTE 1: * Watch out the allocation of "nbuf", if either
* sptr->name * or sptr->sockhost gets changed into pointers instead of *
* directly allocated within the structure... *
*
* NOTE 2: * Function return either a pointer to the structure
* (sptr) or * to internal buffer (nbuf). *NEVER* use the returned
* pointer * to modify what it points!!!
*/
char *
get_client_name(aClient *sptr, int showip)
{
static char nbuf[HOSTLEN * 2 + USERLEN + 7];
char *s = nbuf;
if (MyConnect(sptr))
{
if (sptr->name[0])
s += ircsprintf(s, "%s", sptr->name);
else
s += ircsprintf(s, "<unnamed>", sptr->name);
if (IsServer(sptr))
{
if (showip == TRUE)
s += ircsprintf(s, "[%s]", inetntoa((char *)&sptr->ip));
else if (showip != HIDEME)
s += ircsprintf(s, "[%s]", sptr->sockhost);
}
else if (IsClient(sptr))
{
if (showip == TRUE)
s += ircsprintf(s, "!%s@%s", sptr->user->username,
inetntoa((char *)&sptr->ip));
else if (showip != HIDEME)
s += ircsprintf(s, "!%s@%s", sptr->user->username,
sptr->user->host);
}
else
{
if (showip != HIDEME)
{
s += ircsprintf(s, "([");
if (DoingAuth(sptr))
*s++ = '?';
else if (sptr->flags & FLAGS_GOTID)
*s++ = '+';
else
*s++ = '-';
if (showip == TRUE)
s += ircsprintf(s, "]%s@%s)", sptr->username,
inetntoa((char *)&sptr->ip));
else
s += ircsprintf(s, "]%s@%s)", sptr->username,
sptr->sockhost);
}
}
return nbuf;
}
return sptr->name;
}
/*
* Form sockhost such that if the host is of form user@host, only the
* host portion is copied.
*/
void
get_sockhost(aClient *cptr, char *host)
{
char *s;
if ((s = (char *) strchr(host, '@')))
s++;
else
s = host;
strncpyzt(cptr->sockhost, s, sizeof(cptr->sockhost));
}
/*
* Return wildcard name of my server name according to given config
* entry --Jto
*/
char *
my_name_for_link(char *name, aConnect *aconn)
{
static char namebuf[HOSTLEN];
int count = aconn->port;
char *start = name;
if (count <= 0 || count > 5)
return start;
while (count-- && name)
{
name++;
name = (char *) strchr(name, '.');
}
if (!name)
return start;
namebuf[0] = '*';
strncpy(&namebuf[1], name, HOSTLEN - 1);
namebuf[HOSTLEN - 1] = '\0';
return namebuf;
}
#ifdef DCCALLOW
int remove_dcc_references(aClient *sptr)
{
aClient *acptr;
Link *lp, *nextlp;
Link **lpp, *tmp;
int found;
lp = sptr->user->dccallow;
while(lp)
{
nextlp = lp->next;
acptr = lp->value.cptr;
for(found = 0, lpp = &(acptr->user->dccallow);
*lpp; lpp=&((*lpp)->next))
{
if(lp->flags == (*lpp)->flags)
continue; /* match only opposite types for sanity */
if((*lpp)->value.cptr == sptr)
{
if((*lpp)->flags == DCC_LINK_ME)
{
sendto_one(acptr, ":%s %d %s :%s has been removed from "
"your DCC allow list for signing off",
me.name, RPL_DCCINFO, acptr->name, sptr->name);
}
tmp = *lpp;
*lpp = tmp->next;
free_link(tmp);
found++;
break;
}
}
if(!found)
sendto_realops_lev(DEBUG_LEV, "rdr(): %s was in dccallowme "
"list[%d] of %s but not in dccallowrem list!",
acptr->name, lp->flags, sptr->name);
free_link(lp);
lp = nextlp;
}
return 0;
}
#endif
/*
* NOQUIT
* a method of reducing the stress on the network during server splits
* by sending only a simple "SQUIT" message for the server that is dropping,
* instead of thousands upon thousands of QUIT messages for each user,
* plus an SQUIT for each server behind the dead link.
*
* Original idea by Cabal95, implementation by lucas
*/
void
exit_one_client_in_split(aClient *cptr, aClient *dead, char *reason)
{
Link *lp;
/* send all the quit reasons to all the non-noquit servers we have */
/* yikes. We only want to do this if dead was OUR server. */
/* erm, no, that's not true. Doing that breaks things.
* If a non-noquit server is telling us a server has split,
* we will have already recieved hundreds of QUIT messages
* from it, which will be passed anyway, and this procedure
* will never be called. - lucas
*/
#ifdef NOQUIT
sendto_non_noquit_servs_butone(dead, ":%s QUIT :%s", cptr->name, reason);
#endif
sendto_common_channels(cptr, ":%s QUIT :%s", cptr->name, reason);
while ((lp = cptr->user->channel))
remove_user_from_channel(cptr, lp->value.chptr);
while ((lp = cptr->user->invited))
del_invite(cptr, lp->value.chptr);
while ((lp = cptr->user->silence))
del_silence(cptr, lp->value.cp);
if (cptr->user->alias)
cptr->user->alias->client = NULL;
if (cptr->ip.s_addr)
clones_remove(cptr);
#ifdef RWHO_PROBABILITY
probability_remove(cptr);
#endif
#ifdef DCCALLOW
remove_dcc_references(cptr);
#endif
del_from_client_hash_table(cptr->name, cptr);
hash_check_watch(cptr, RPL_LOGOFF);
remove_client_from_list(cptr);
}
/* exit_one_server
*
* recursive function!
* therefore, we pass dead and reason to ourselves.
* in the beginning, dead == cptr, so it will be the one
* out of the loop last. therefore, dead should remain a good pointer.
* cptr: the server being exited
* dead: the actual server that split (if this belongs to us, we
* absolutely CANNOT send to it)
* from: the client that caused this split
* lcptr: the local client that initiated this
* spinfo: split reason, as generated in exit_server
* comment: comment provided
*/
void
exit_one_server(aClient *cptr, aClient *dead, aClient *from,
aClient *lcptr, char *spinfo, char *comment)
{
aClient *acptr, *next;
DLink *lp;
/* okay, this is annoying.
* first off, we need two loops.
* one: to remove all the clients.
* two: to remove all the servers.
* HOWEVER! removing a server may cause removal of more servers
* and more clients.
* and this may make our pointer to next bad. therefore, we have to restart
* the server loop each time we find a server.
* We _NEED_ two different loops: all clients must be removed "
* before the server is
* removed. Otherwise, bad things (tm) can happen.
*/
Debug((DEBUG_NOTICE, "server noquit: %s", cptr->name));
for (acptr = client; acptr; acptr = next)
{
next = acptr->next; /* we might destroy this client record
* in the loop. */
if(acptr->uplink != cptr || !IsPerson(acptr))
continue;
exit_one_client_in_split(acptr, dead, spinfo);
}
for (acptr = client; acptr; acptr = next)
{
next = acptr->next; /* we might destroy this client record in
* the loop. */
if(acptr->uplink != cptr || !IsServer(acptr))
continue;
exit_one_server(acptr, dead, from, lcptr, spinfo, comment);
next = client; /* restart the loop */
}
Debug((DEBUG_NOTICE, "done exiting server: %s", cptr->name));
for (lp = server_list; lp; lp = lp->next)
{
acptr = lp->value.cptr;
if (acptr == cptr || IsMe(acptr) ||
acptr == dead || acptr == lcptr)
continue;
/* if the server is noquit, we only want to send it
* information about 'dead'
* if it's not, this server gets split information for ALL
* dead servers.
*/
#ifdef NOQUIT
if(IsNoquit(acptr))
#endif
if(cptr != dead)
continue;
if (cptr->from == acptr) /* "upstream" squit */
sendto_one(acptr, ":%s SQUIT %s :%s", from->name, cptr->name,
comment);
else
sendto_one(acptr, "SQUIT %s :%s", cptr->name, comment);
}
del_from_client_hash_table(cptr->name, cptr);
hash_check_watch(cptr, RPL_LOGOFF);
remove_client_from_list(cptr);
}
/* exit_server
*
* lcptr: the local client that initiated this
* cptr: the server that is being dropped.
* from: the client/server that caused this to happen
* comment: reason this is happening
* we then call exit_one_server, the recursive function.
*/
void exit_server(aClient *lcptr, aClient *cptr, aClient *from, char *comment)
{
char splitname[HOSTLEN + HOSTLEN + 2];
#ifdef HIDE_SPLIT_SERVERS
ircsprintf(splitname, "%s %s", HIDDEN_SERVER_NAME, HIDDEN_SERVER_NAME);
#else
ircsprintf(splitname, "%s %s", cptr->uplink->name, cptr->name);
#endif
Debug((DEBUG_NOTICE, "exit_server(%s, %s, %s)", cptr->name, from->name,
comment));
exit_one_server(cptr, cptr, from, lcptr, splitname, comment);
}
/*
* exit_client
* This is old "m_bye". Name changed, because this is not a
* protocol function, but a general server utility function.
*
* This function exits a client of *any* type (user, server, etc)
* from this server. Also, this generates all necessary prototol
* messages that this exit may cause.
*
* 1) If the client is a local client, then this implicitly exits
* all other clients depending on this connection (e.g. remote
* clients having 'from'-field that points to this.
*
* 2) If the client is a remote client, then only this is exited.
*
* For convenience, this function returns a suitable value for
* m_function return value:
*
* FLUSH_BUFFER if (cptr == sptr)
* 0 if (cptr != sptr)
*/
int
exit_client(aClient *cptr, aClient *sptr, aClient *from, char *comment)
{
#ifdef FNAME_USERLOG
time_t on_for;
#endif
if (MyConnect(sptr))
{
call_hooks(CHOOK_SIGNOFF, sptr);
if (IsUnknown(sptr))
Count.unknown--;
if (IsAnOper(sptr))
remove_from_list(&oper_list, sptr, NULL);
if (sptr->flags & FLAGS_HAVERECVQ)
{
/* mark invalid, will be deleted in do_recvqs() */
DLink *lp = find_dlink(recvq_clients, sptr);
if (lp)
lp->flags = -1;
}
if (IsClient(sptr))
Count.local--;
if (IsNegoServer(sptr))
sendto_realops("Lost server %s during negotiation: %s",
sptr->name, comment);
if (IsServer(sptr))
{
Count.myserver--;
if (IsULine(sptr))
Count.myulined--;
remove_from_list(&server_list, sptr, NULL);
if (server_list == NULL)
server_was_split = YES;
}
sptr->flags |= FLAGS_CLOSING;
if (IsPerson(sptr))
{
Link *lp, *next;
LOpts *lopt = sptr->user->lopt;
/* poof goes their watchlist! */
hash_del_watch_list(sptr);
/* if they have listopts, axe those, too */
if(lopt != NULL)
{
remove_from_list(&listing_clients, sptr, NULL);
for (lp = lopt->yeslist; lp; lp = next)
{
next = lp->next;
MyFree(lp->value.cp);
free_link(lp);
}
for (lp = lopt->nolist; lp; lp = next)
{
next = lp->next;
MyFree(lp->value.cp);
free_link(lp);
}
MyFree(sptr->user->lopt);
sptr->user->lopt = NULL;
}
sendto_realops_lev(CCONN_LEV,
"Client exiting: %s (%s@%s) [%s] [%s]",
sptr->name, sptr->user->username,
sptr->user->host,
(sptr->flags & FLAGS_NORMALEX) ?
"Client Quit" : comment,
sptr->hostip);
sendto_serv_butone(NULL, ":%s CONOPS :Client exiting: %s (%s@%s) [%s] [%s]",
me.name, sptr->name, sptr->user->username,
sptr->user->host, (sptr->flags & FLAGS_NORMALEX) ? "Client Quit" :
comment, sptr->hostip);
}
#ifdef FNAME_USERLOG
on_for = timeofday - sptr->firsttime;
#endif
#if defined(USE_SYSLOG) && defined(SYSLOG_USERS)
if (IsPerson(sptr))
syslog(LOG_NOTICE, "%s (%3d:%02d:%02d): %s!%s@%s %d/%d\n",
myctime(sptr->firsttime),
on_for / 3600, (on_for % 3600) / 60,
on_for % 60, sptr->name,
sptr->user->username, sptr->user->host,
sptr->sendK, sptr->receiveK);
#endif
#if defined(FNAME_USERLOG)
{
char linebuf[300];
static int logfile = -1;
static long lasttime;
/*
* This conditional makes the logfile active only after it's
* been created - thus logging can be turned off by removing
* the file.
*
* stop NFS hangs...most systems should be able to open a file in
* 3 seconds. -avalon (curtesy of wumpus)
*
* Keep the logfile open, syncing it every 10 seconds -Taner
*/
if (IsPerson(sptr))
{
if (logfile == -1)
{
alarm(3);
logfile = open(FNAME_USERLOG, O_WRONLY | O_APPEND);
alarm(0);
}
ircsprintf(linebuf, "%s (%3d:%02d:%02d): %s!%s@%s %d/%d\n",
myctime(sptr->firsttime), on_for / 3600,
(on_for % 3600) / 60, on_for % 60,
sptr->name, sptr->user->username,
sptr->user->host, sptr->sendK, sptr->receiveK);
alarm(3);
write(logfile, linebuf, strlen(linebuf));
alarm(0);
/* Resync the file evey 10 seconds*/
if (timeofday - lasttime > 10)
{
alarm(3);
close(logfile);
alarm(0);
logfile = -1;
lasttime = timeofday;
}
}
}
#endif
if (sptr->fd >= 0)
{
if (cptr != NULL && sptr != cptr)
sendto_one(sptr, "ERROR :Closing Link: %s %s (%s)",
IsPerson(sptr) ? sptr->sockhost : "0.0.0.0",
sptr->name, comment);
else
sendto_one(sptr, "ERROR :Closing Link: %s (%s)",
IsPerson(sptr) ? sptr->sockhost : "0.0.0.0",
comment);
}
/*
* * Currently only server connections can have * depending
* remote clients here, but it does no * harm to check for all
* local clients. In * future some other clients than servers
* might * have remotes too... *
*
* Close the Client connection first and mark it * so that no
* messages are attempted to send to it. *, The following *must*
* make MyConnect(sptr) == FALSE!). * It also makes sptr->from ==
* NULL, thus it's unnecessary * to test whether "sptr != acptr"
* in the following loops.
*/
if (IsServer(sptr))
{
sendto_ops("%s was connected for %lu seconds. %lu/%lu "
"sendK/recvK.", sptr->name, timeofday - sptr->firsttime,
sptr->sendK, sptr->receiveK);
#ifdef USE_SYSLOG
syslog(LOG_NOTICE, "%s was connected for %lu seconds. %lu/%lu "
"sendK/recvK.", sptr->name,
(u_long) timeofday - sptr->firsttime,
sptr->sendK, sptr->receiveK);
#endif
close_connection(sptr);
sptr->sockerr = 0;
sptr->flags |= FLAGS_DEADSOCKET;
}
else
{
close_connection(sptr);
sptr->sockerr = 0;
sptr->flags |= FLAGS_DEADSOCKET;
}
}
exit_one_client(cptr, sptr, from, comment);
return cptr == sptr ? FLUSH_BUFFER : 0;
}
/*
* Exit one client, local or remote. Assuming all dependants have
* been already removed, and socket closed for local client.
*/
static void
exit_one_client(aClient *cptr, aClient *sptr, aClient *from, char *comment)
{
Link *lp;
/*
* For a server or user quitting, propogate the information to
* other servers (except to the one where is came from (cptr))
*/
if (IsMe(sptr))
{
sendto_ops("ERROR: tried to exit me! : %s", comment);
return; /* ...must *never* exit self!! */
}
else if (IsServer(sptr))
{
#ifdef ALWAYS_SEND_DURING_SPLIT
currently_processing_netsplit = YES;
#endif
exit_server(cptr, sptr, from, comment);
#ifdef ALWAYS_SEND_DURING_SPLIT
currently_processing_netsplit = NO;
#endif
return;
}
else if (!(IsPerson(sptr)))
/*
* ...this test is *dubious*, would need * some thought.. but for
* now it plugs a * nasty hole in the server... --msa
*/
; /* Nothing */
else if (sptr->name[0])
{
/* ...just clean all others with QUIT... */
/*
* If this exit is generated from "m_kill", then there is no
* sense in sending the QUIT--KILL's have been sent instead.
*/
if ((sptr->flags & FLAGS_KILLED) == 0)
{
sendto_serv_butone(cptr, ":%s QUIT :%s",
sptr->name, comment);
}
/*
* * If a person is on a channel, send a QUIT notice * to every
* client (person) on the same channel (so * that the client can
* show the "**signoff" message). * (Note: The notice is to the
* local clients *only*)
*/
if (sptr->user)
{
send_part_to_common_channels(sptr, comment);
send_quit_to_common_channels(sptr, comment);
while ((lp = sptr->user->channel))
remove_user_from_channel(sptr, lp->value.chptr);
if (sptr->ip.s_addr)
clones_remove(sptr);
#ifdef RWHO_PROBABILITY
probability_remove(sptr);
#endif
/* Clean up invitefield */
while ((lp = sptr->user->invited))
del_invite(sptr, lp->value.chptr);
/* Clean up silences */
while ((lp = sptr->user->silence))
del_silence(sptr, lp->value.cp);
#ifdef DCCALLOW
remove_dcc_references(sptr);
#endif
/* again, this is all that is needed */
}
}
/* Remove sptr from the client list */
if (del_from_client_hash_table(sptr->name, sptr) != 1)
{
Debug((DEBUG_ERROR, "%#x !in tab %s[%s] %#x %#x %#x %d %d %#x",
sptr, sptr->name,
sptr->from ? sptr->from->sockhost : "??host",
sptr->from, sptr->next, sptr->prev, sptr->fd,
sptr->status, sptr->user));
}
/* remove user from watchlists */
if(IsRegistered(sptr))
hash_check_watch(sptr, RPL_LOGOFF);
remove_client_from_list(sptr);
return;
}
void
initstats()
{
memset((char *) &ircst, '\0', sizeof(ircst));
}
char *
make_parv_copy(char *pbuf, int parc, char *parv[])
{
int pbpos = 0, i;
for(i = 1; i < parc; i++)
{
char *tmp = parv[i];
if(i != 1)
pbuf[pbpos++] = ' ';
if(i == (parc - 1))
pbuf[pbpos++] = ':';
while(*tmp)
pbuf[pbpos++] = *(tmp++);
}
pbuf[pbpos] = '\0';
return pbuf;
}
syntax highlighted by Code2HTML, v. 0.9.1