|
Table of Contents Overview...........................................1 Major Features.................................2 Feature Design Specification..................3 Module Design.........................................4 Socket Structure...................................................................................................4 Message Structures...............................................................................................5 Database Blobs.......................................................................................................6 Configuration Module............................................................................................6 Event Logging.......................................................................................................6 Demonstration Design..........................8 Administration......................................9 Appendices.........................................10 A-1. Calendar.....................................11 A-2. Listings.......................................12 Socket Structure...................................................................................................12 Socket Interface API................................................................................................12 Creating A Listening Server.....................................................................................13 Support Functions for Socket API.............................................................................13 Abbreviated List of Unique Message Identifiers...................................................13 General Message Structures................................................................................14 Message Specific Structures.................................................................................14 Message Type To Handler Correlation Using A Jump Table..........................................15 Database API..........................................................................................................15 User Table Schema................................................................................................15 Sample gicqd Configuration File...........................................................................16 Initializing and Starting Event Logging................................................................17 Sample Event Log.................................................................................................17 Directory Layout....................................................................................................18 A-3. Figures...............................................19 gicqd overall structure.........................................................................................19 database structure.................................................................................................19 server structure......................................................................................................19 | ||
I. Overview | ||
|
ICQ is a wide-area communication system whose core design is remarkably similar to Athena Zephyrs. ICQ goes a few steps further by integrating direct client-to-client (DCC) email, voice mail, and file transfer capabilities directly into the ICQ client, thus making communication between users more natural and conversational. Since its inception in November of 1996 by Mirabilis, Inc., ICQ, which was derived from the term "I Seek You," has "taken the Internet by storm" according to AOL Online, who recently purchased Mirabilis.
In our research, we discovered that the original, copyrighted ICQ client had been cloned to run on many different platforms, but a compatible server had yet to be cloned. Knowing this and also seeing that the Free Software Foundation`s GNU project has such a server listed as one of their needs, we decided to clone the server. Our mission, therefore, is to design and create a free ICQ-compatible server which we hope will be widely distributed to create an option for company-wide messaging systems, and greater deployment of the ICQ system.
Our goal is to create a compatible server that provides all of the functionality of the copyrighted server as well as more. Additionally, we will strive to provide extensibility, configurability, localization and internationalization and most importantly freedom of code. To this end, we have notified GNU of our selection of their ICQ-compatible server need. We have also contacted the ICQ development newsgroup (ICQ-devel) and informed them of our project. Both GNU and ICQ-devel fully support us on this project. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
II. Major Features | ||
|
| ||
|
To participate on the ICQ network, the server will have to implement a basic set of functions. Firstly, it will have to implement sockets to allow messages to be sent to and received from clients. Secondly, the server will have to provide functions for decoding the received messages and for encoding the response messages. Finally, the server will have to support database access into several tables and will be responsible for initializing those tables.
To address GNU requirements and to make the code generally more robust, the server will implement additional installation, customization and portability functionality. To aid in the building of the server, a GNU configure script will be provided to allow for per-site customization of the server. Once built, the server will use the standard make install procedure to install the software in normal locations, with GNU extensions being provided to override default installation.
Past the installation stage, the server will implement a standard GNU command-line interface. Through the use of a configuration file, server users can modify critical program values without having to recompile. Security options such as turning on and off of server proxying, enabling or disabling packet encryption, and host-based access control lists can be set and changed through the configuration file and may be implemented in future versions of the server. Detailed logging to both a server-specific log file and the system log will be provided for any debugging necessary once the server is distributed at a user's site. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
III. Feature Design Specification | ||
|
| ||
|
Our GNU ICQ-compatible server, gicqd, is composed primarily of a server object and a database object; see Figure 1. The server reacts to incoming messages on its well-known port by accessing the database to log users in, store offline messages, mirror tables, etc. In our model, client messages initiate actions in the server; the server does not proactively initiate any form of data manipulation.
The server is configured at startup or upon receiving a hangup signal (SIGHUP) by consulting its configuration file (see Figure 2). The server reads this configuration file and picks out directives and associated data. Information such as the port to listen on, log file location, etc. are stored in this file. Once configured, the server will open its log file and then wait for messages on its socket. Whenever a message is received, the server immediately forks to handle the message; no support is presently planned to make the server multi-threaded.
The server children are responsible for handling the message, while the parent returns to listen for more messages. The parent never interacts directly with the database; rather, it is the children who handle the database manipulations. The children are also responsible for responding to the client.
The database itself is broken up into tables, each of which is subsequently broken up into records; see Figure 3. Tables can be flat files or possibly their own partitions. We specifically do not concern ourselves with how the tables are stored, as we rely on the published APIs to the various supported databases; more will be said on the database in section IV, Module Design. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
IV. Module Design | ||
|
| ||
A. Primary ConstructsThere are two primary constructs on which the ICQ-specific portion of our server is built: the socket and the message structures. The socket structure provides an encapsulation of a socket connection's termed a session information. The message structures define how a particular packet is laid and for all supported messages there is a unique message structure.
i. Socket StructureThe socket structure is defined as in Listing 1. Pertinent information regarding a socket's definition and state is stored in the SOCKET structure. Functions operating on a SOCKET pointer are provided to manipulate a socketed connection. These interface functions wrap around low-level system calls such as socket(), accept(), etc. and are given in Listing 2.
These interface functions take a step back from the concept of a socket and provide a look at connections from a client and server level. Sample code is provided in Listing 3 to demonstrate the ease of creating a server and receiving client messages for that server.
Several support functions have been included with the SOCKET API to help in finding out common information from Internet information; consult Listing 4 for the list. IsIP() determines if the given C-Style string is an IP address given in dotted-quad (192.100.100.100, for example) notation. The function does more than just check to see if the first digit is a numeral, as is common in many software packages. IsIP() calls Inet_aton() to decide if the string is an IP or not; the problem, however, is that the system call inet_aton() is not available on all machines. As such, a POSIX implementation for inet_aton() is provided by our code if inet_aton() is not detected during the build.
The remaining two support functions house little magic. Hostname() returns the current hostname, if available, or the current host's IP otherwise. Address() will return the IP of the specified hostname. All of the support functions, as well as the rest of our functions, are written to be thread-safe provided | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
|
that the code is compiled with _REENTRANT defined. POSIX or Solaris thread machines are detected during the build and _REENTRANT is defined appropriately.
ii. Message StructuresEvery ICQ message has a unique identifier that tells the server what kind of message it is. In addition to these identifiers, messages are all variable length and contain message-dependent data. Message identifiers are given in a list of defines as shown in Listing 5. Notice that message identifiers are two byte numbers and are stored in little-Endian format. The ICQ protocol requires that all ICQ packets be transmitted in little-Endian order, not big-Endian, as is used everywhere else on the Internet.
In addition to having a unique identifier, every message also has a general format. This format is represented by the structures given in Listing 6. The format for client messages is simple: a header, consisting of version, unique message identifier and packet sequence number is followed by the UIN of the client. Any additional message-specific information comes after the UIN. Server messages are nearly identical: the same header as found in client messages is followed by the message-specific data. Notice that the server does not send a UIN to the client.
The message-specific information also have structures associated with them; see Listing 7. Based on the message type (as found in the header), the appropriate structure is imposed on top of the message-specific data to give us fields of data on which to operate. To support the decoding of messages each particular type of message is given a function to handle the parsing of the packet. Correlation between message type and handling function is maintained through the use of a jump table, as seen in Listing 8.
B. Additional ConstructsAside from the ICQ-specific details of our server, there are three other primary constructs which make our server work. The first is the database record (called a "blob") which houses client information and server state information. The second is the configuration module which reads and parses the gicqd configuration file upon start up. Finally is the event logging and history mechanism. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
|
iii. Database BlobsDatabase support is provided through Berkeley, GNU DB, and traditional UNIX DBM facilities. At build time, the user can select to use Berkeley DB for its speed and smaller sizes. If the user elects not to use this support, then the configuration process will check to first see if GNU DB is supported and, failing that, will check for DBM. An API is provided to encapsulate the lower level calls associated with each DB methodology as seen in Listing 9. Open, Close, Get and Put methods are supported; we do not plan to offer Rollback capability.
Information is stored in the database as a simple string of bytes. To convert the string of bytes into usable fields, a database schema was created in C structures; see Listing 10 for the User table schema. These schema, when overlain on the string of bytes, creates what is termed a database blob. Information in each blob is then accessible by accessing individual members of the structure.
iv. Configuration ModuleServer configuration is provided through a server configuration file that is read and parsed at start-up or when the server receives a SIGHUP; the sample gicqd configuration file is provided in Listing 11. The configuration parsing functions are responsible for stripping out comments and storing the directive's information in the correct program variable.
v. Event LoggingThe event logging mechanism is the final construct our server uses extensively throughout. Event logging is started in the initialization routine of the program and is called only at start-up; Listing 12 provides the usage in our program. All event logging macro names are preceded by EVENT to help mark them to the eye; e.g., EVENT_START(), EVENT(), etc. By default, the event logging mechanism will write to standard error, but can be changed by calling EVENT_SETLOG() as is done in Listing 12. Critical errors that must be shown to the user can be accessed through the CONSOLE() macro; this macro writes the text of the message to standard error regardless of the log device. Additionally, the DEBUG() macro is provided for events that should be explicitly noted as being for debug only. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
|
Every event has associated with it a severity level. The levels that we have defined are: none, debug, information, warning, minor, major, failure, critical, system log, assertion, and user-defined. Each level is ranked according to severity, with none being the lowest and user-defined being the highest. The programmer can specify a filter so that only events with a certain level or higher can get through. However, critical, system log, assertion and user-defined events always get through any filter applied.
Events have the general format as given in Listing 13. The first line, preceded by a hash ("#") contains the event number, the date and time, the severity both as a number and as text and the number of bytes in the event message. The second line is the actual message itself. Non-printable characters in the message are converted to hex before display to guarantee that the log file never contains binary data. The last line of the event gives the process ID that generated the event, plus the line number and file where the event is.
The final aspect of logging is crucial to a usable log system. We have implemented our event logging code so that multiple processes can not simultaneously write to the log file. At build time, the configuration script determines which of the four standard C advisory locking functions flock(), fcntl(), lockf(), flockfile() are implemented on the target system and compiles in support for one. The build process will try for flock() first, then fcnt(), lockf() and finally flockfile(). If none of these facilities are available, then no file locking is provided. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
V. Demonstration Design | ||
|
| ||
|
For our demonstration, we will have multiple instances of the ICQ client set up on a PC and our server on a different, non-PC machine. We will demonstrate how the server allows clients to login, notifies clients when particular clients login, saves messages for offline clients and finally sends stored messages to clients. We will then display two clients communicating between each other to verify that the server sends each client the information necessary to talk to the corresponding client. We wish to use the client on a little-endian machine and the server on a big-endian machine to show that byte-ordering is handled correctly. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
VI. Administration | ||
|
The development of gicqd will proceed on an isolated Linux 2.0.36 machine that is not available from the Internet. This Linux machine is connected to a network of other machines of various UNIX flavors AIX, HP-UX, Solaris, etc. that will be used to test the software on.
Source code control is managed via a Concurrent Version Systems (CVS) tree. Our CVS tree, like our development machine, is not publicly accessible via the Internet. The primary documentation for gicqd resides in the CVS repository alongside the source code. These documents are in text format for frequently accessed files (README, NOTES, TODO, COPYING, INSTALL, etc.) and tex format for all other files, such as the user manual. To meet with GNU standards, we must distribute our documentation in tex format, and as such, appropriate viewers will be required.
The layout of the project directory follows the typical GNU project structure (see Listing 14). The project root directory ($PROJDIR) houses the installation and configuration documentation along with GNU configuration scripts and m4 macros. The server configuration file, gicqd.conf, resides in the $PROJDIR/conf/ directory. Tex documentation will eventually be located in the $PROJDIR/docs/ directory, but presently a pointer is provided to our website's documentation. The remaining directories incl, lib, and src contain the gicqd includes, libraries and source, respectively. | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Appendices | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
A-1. Calendar | ||
|
January Fri, 08th: project descriptionTue, 12th: begin design phase; website created; confer with GNU Wed, 20th: infrastructure coding mostly there Fri, 29th: diagram client-server interaction
February Mon, 01st: rough draft of proposal dueTue, 02nd: solidify server design Fri, 05th: draft proposal presentation Tue, 09th: primary messages put in C-struct form Tue, 09th: login packet working; complete client-server negotiation Fri, 12th: final draft of proposal due Fri, 26th: login packet tied in with database; user's contact list is updated March Mon, 01st: multi-user testing possible; version 0.1.0 (alpha)Wed, 03rd: offline message store code in-place Fri, 05th: rough draft of design due Fri, 05th: user state change (login, logout, etc.) sent to other clients Mon, 15th: design presentations begin Fri, 19th: installation works (ie, make install) Mon, 22nd: design presentations end Mon, 22nd: dbm/ndbm code working Wed, 24th: offline message store code works Fri, 26th: final draft of design due Fri, 26th: servers can proxy (ie-mirror) databases Wed, 30th: beta-test coding completed; approx. version 0.1.1 (beta)
April Thu, 01st: add alarms where appropriate to receive/send callsFri, 16th: code robustness, full packet support Mon, 19th: end testing phase Fri, 23rd: projected completion date; version 0.1.2 (release) Fri, 23rd: project documentation due Mon, 26th: final presentations begin Fri, 30th: final presentations end | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
A-2. Listings | ||||
Listing 1Socket Structure | ||||
typedef struct {long host_inad; /* Net byte-order inet address */ long dest_inad; /* Net byte-order inet address */ char host_addr[MAXHOSTNAMELEN+1]; /* Resolved name, if exist */ char dest_addr[MAXHOSTNAMELEN+1]; /* Resolved name, if exist */ unsigned int host_port; /* port host is listening on */ unsigned int dest_port; /* port peer is listening on */ int protocol; /* Protocol (UDP, TCP, etc) */ int type; /* Type (DGRAM, STREAM, etc) */ int family; /* Family (AF_INET, AF_UNIX) */ } NETWORK;
typedef struct { int fd; /* Socket file descriptor */ NETWORK n; } SOCKET; | ||||
Listing 2Socket Interface API | ||||
int CreateSocket(SOCKET *);int CloseSocket(SOCKET *); int LocalizeSocket(SOCKET *); int SetSocketConnection(SOCKET *, const char *, unsigned int); int SetSocketHost(SOCKET *, const char *); int SetSocketPeer(SOCKET *, const char *); void SetSocketHostPort(SOCKET *, unsigned int); void SetSocketPeerPort(SOCKET *, unsigned int); int SetSocketProtocol(SOCKET *, const char *); int SetSocketType(SOCKET *, int);
int SocketDaemon(SOCKET *); int SocketDaemonAcceptClient(SOCKET *, SOCKET *); void StopSocketDaemon(SOCKET *);
int SocketConnect(SOCKET *); int SocketReceiveMsg(SOCKET *, SOCKET *, char *, size_t); int SocketSendMsg(const SOCKET *, const char *, size_t); | ||||
|
GNU ICQ-Compatible Server Design Specification Document
| ||||
Listing 3Creating A Listening Server | ||
/* NOTE: checking of return codes is omitted for brevity */void start() { SOCKET server, client; char msg[512+1]; /* arbitrarily large */
SetSocketHost(&server, (char *)NULL); /* set server host to localhost */ SetSocketHostPort(&server, 4000); /* set server port to 4000 */ SetSocketProtocol(&server, "udp"); /* set protocol to UDP NOTE: this function calls getprotobyname() */ SetSocketType(&server, SOCK_DGRAM); /* set protocol family */
/* start the daemon */ SocketDaemon(&server);
for (;;) { /* block until a message is received */ SocketReceiveMsg(&server, &client, msg, sizeof(msg)); }
} | ||
Listing 4Support Functions for Socket API | ||
int IsIP(const char *);int Inet_aton(const char *, struct in_addr *); int Hostname(char *, size_t); int Address(char *, size_t, const char *); | ||
Listing 5Abbreviated List of Unique Message Identifiers | ||
/* commands as sent by the client */#define CLNT_ADD_REQ 0x04EC #define CLNT_ADD_REG 0x03FC #define CLNT_ADD_INFO 0x04A6 #define CLNT_QUERY_SRVRS 0x04BA #define CLNT_QUERY_ADDONS 0x04C4 /* ... more */
/* commands as sent by the server */ #define SRVR_RPLY_REQ_INFO 0x0118 #define SRVR_RPLY_UPD_INFO 0x01E0 #define SRVR_SYS_MSG 0x01C2 #define SRVR_UIN_CHANGE 0x01A4 /* ... more */ | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Listing 6General Message Structures | ||
typedef struct{ BYTE version[2]; BYTE command[2]; BYTE num[2]; } header_t;
typedef struct _uin_t { BYTE uin[4]; } uin_t;
typedef struct _clnt_message_t { header_t header; uin_t uin; BYTE data[MAXDATA_SZ]; } clnt_message_t;
typedef struct _srvr_message_t { header_t header; BYTE data[MAXDATA_SZ]; } srvr_message_t; | ||
Listing 7Message Specific Structures | ||
/* this is an abbreviated list... */typedef struct _clnt_contact_t { BYTE num[1]; /* XXX: NOTE: the spec says that this should be 2 bytes, but the kicq (ie, libicq thing) only sends it as one byte so I'm using that here */ BYTE list[CONTACT_MAX][4]; } clnt_contact_t;
typedef struct _clnt_upd_status_t { BYTE status; } clnt_upd_status_t;
/* packets as sent by the server */ typedef struct _srvr_rply_login_t { BYTE uin[4]; BYTE ip[4]; BYTE seqnum[2]; BYTE XXX_X1[4]; /* XXX: unknown */ BYTE XXX_X2[4]; /* XXX: unknown */ BYTE XXX_X3[4]; /* XXX: unknown */ BYTE XXX_X4[4]; /* XXX: unknown */ BYTE XXX_X5[6]; /* XXX: unknown */ } srvr_rply_login_t; | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Listing 8Message Type To Handler Correlation Using A Jump Table | ||
struct MJT{ int id; VFUNC func; char *desc; };
const struct MJT mjt[] = { {CLNT_ACK, handle_clnt_ack, "acknowledgement (sent by client)"}, {CLNT_KEEPALIVE, handle_clnt_keepalive, "keep-alive notification (sent by client)"}, {CLNT_LOGIN, handle_clnt_login, "client login (sent by client)"}, {CLNT_UPD_STATUS, handle_clnt_upd_status, "client has changed status (sent by client)"}, {CLNT_LOGIN_X1, handle_clnt_login_x1, "client login X1 (sent by client)"}, {CLNT_CONTACT, handle_clnt_contact, "client contact list (sent by client)"}, {CLNT_SND_TEXT, handle_clnt_snd_text, "client sent text; usually for going offline (sent by client)"},
/* no entries below this point */ {-1, NULL, NULL} }; | ||
Listing 9Database API | ||
int DB_Open(int);void DB_Close(void); int DB_Put(int, char *); int DB_Get(int, char *); | ||
Listing 10User Table Schema | ||
typedef struct _DBREC_USERINFO{ /* basic information */ long int uin; char nick[128+1]; /* arbitrarily large */ char last[1024+1]; /* arbitrarily large */ char first[1024+1]; /* arbitrarily large */ char email[1024+1]; /* arbitrarily large */
/* "extended" information */ char city[1024+1]; /* arbitrarily large */ char state[1024+1]; /* arbitrarily large; USA only */ int country; /* -1 if not supplied */ int age; /* -1 if not supplied */ short sex; /* 0 if not supplied */ char phone[1024+1]; /* arbitrarily large */ char hpage[1024+1]; /* arbitrarily large */ char about[2048+1]; /* arbitrarily large */ } DBREC_USERINFO; | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Listing 11Sample gicqd Configuration File | ||
################################################################################ default gicqd configuration file # <bishop@ateb.com> : Sat Jan 23 00:38:29 EST 1999 # # This is the default configuration file for gicqd. # # Comments are, as you can see, traditional hash comments. # # The general form of a directive is much like httpd, et. al.: # DirectiveName Value # AnotherDirective "your choice of value" # # Directive names are typically mixed-case form (e.g., # ADirectiveIsNamedLikeThis) but is case insensitive. They are, however, # always one word. # ###############################################################################
############################################################################### # Directive : Server # Description : The state of the server. # Default : Standalone # Notes : <Standalone | Inetd> are only valid options. ############################################################################### ###Server Standalone Server Inetd
############################################################################### # Directive : LogFile # Description : The location of the log file; /dev/null if you don't care # about logging (sorry XXX-Gaters, get a real OS) # Default : ${GICQD_SERVER_ROOT}/var/gicqd.log # Notes : A relative path here will be relative to the server root. ############################################################################### LogFile /tmp/gicqd.log
############################################################################### # Directive : PidFile # Description : The location of the pid file; again, /dev/null if you don't # care. # Default : ${GICQD_SERVER_ROOT}/var/gicqd.pid # Notes : A relative path here will be relative to the server root. ############################################################################### PidFile /tmp/gicqd.pid
############################################################################### # Directive : Port # Description : The port clients will connect to. # Default : 4000 ############################################################################### ###Port 4000 | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Listing 12Initializing and Starting Event Logging | ||
/*************************************************************************** initialize event subsystem * set filter level **************************************************************************/ EVENT_INIT(); #ifndef _DEBUG EVENT_SETFILTER(ES_WARN); #endif
/* unrelated code skipped */
#ifndef _DEBUG /************************************************************************** * set log file **************************************************************************/ EVENT_SETLOG(logpath); if (LOG_TYPE() != LOG_FILE) { CONSOLE(("can't write to log file [%s] (%d %s)", logpath, errno, strerror(errno)));
return(ERR_INIT); /* NOTREACHED */ } #endif /************************************************************************** * start event subsystem **************************************************************************/ EVENT_START();
/* example usage (not in actual gicqd code) */ EVENT((ES_INFO, "this is an information event!")); | ||
Listing 13Sample Event Log | ||
# 7: (01-Mar-1999 07:18:48.5692) sev=2 (Information) bytes=15-> logging started -> pid=16665, line=193 of gicqd.c # 8: (01-Mar-1999 07:18:48.5816) sev=2 (Information) bytes=43 -> server listening on maelstrom.ateb.com:4000 -> pid=16665, line=631 of gicqd.c | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
Listing 14Directory Layout | ||
$ ls -R $PROJDIRAUTHORS Makefile.in conf/ configure* missing* COPYING NEWS config.cache configure.in mkinstalldirs* ChangeLog README config.guess@ docs/ src/ INSTALL TODO config.log incl/ Makefile WEBSITE config.status* install-sh* Makefile.am aclocal.m4 config.sub@ lib/
conf: gicqd.conf
docs: REDIRECT
incl: config.h event.h gicqd_db.h net.h stamp-h utils.h config.h.in gicqd.h message.h os.h sysdefs.h
lib: Makefile Makefile.in libos.c libutils.c Makefile.am libnet.c libstandard.c
src: Makefile Makefile.am Makefile.in gicqd.c gicqd_db.c message.c | ||
|
GNU ICQ-Compatible Server Design Specification Document
| ||
A-3. Figures | ||||||||||||||||||||||||||
|
server | ||||||||||||||||||||||||||
|
database | ||||||||||||||||||||||||||
|
| ||||||||||||||||||||||||||
|
server | ||||||||||||||||||||||||||
Figure 1gicqd overall structure | ||||||||||||||||||||||||||
|
| ||||||||||||||||||||||||||
|
config file | ||||||||||||||||||||||||||
|
log file | ||||||||||||||||||||||||||
|
| ||||||||||||||||||||||||||
|
| ||||||||||||||||||||||||||
|
database |
records | |||||||||||||||||||||||||
|
|
Figure 2server structure | |||||||||||||||||||||||||
|
|
table | |||||||||||||||||||||||||
Figure 3database structure | ||||||||||||||||||||||||||
|
GNU ICQ-Compatible Server Design Specification Document
| ||||||||||||||||||||||||||