libdorrit(3) FreeBSD Library Functions Manual libdorrit(3) NAME libdorrit – UNIX-Domain WebSocket Server Library SYNOPSIS #include -I/usr/local/include -L/usr/local/lib -ldorrit DESCRIPTION Libdorrit implements a generic event-driven WebSocket server that works with the prospero(8) web server. Dorrit performs network and concurrency tasks. You supply code to service connections. Dorrit provides no mechanism to set events on arbitrary descriptors. • Dorrit accepts only local UNIX-domain connections. • Dorrit automatically responds to pings with pongs. • Dorrit drops connections with clients that send: • Frames with payloads and no masking keys. • Frames with payloads larger than a specified length, which can be no greater than 63535. • Fragment frames. Incoming messages must not be fragmented. An example echo server is included in the dorrit source distribution. USAGE The library provides your server's "main" function. You define 5 functions to match the following prototypes. void ws_init_func(); void ws_exit_func(); int ws_open_callback( void *, char * ); void ws_read_callback( void *, int, int, unsigned char * ); void ws_close_callback( void * ); Do not define any other global symbol beginning with the 3 characters ´ws_' because the library reserves that namespace. WS_INIT_FUNC() In ws_init_func() perform the initialization tasks your server needs to do once at server start-up. Dorrit calls ws_init_func(): • before attempting to change the server's user and group to the values specified by the command line options. If the server starts as root ws_init_func(), executes as root. • before the server becomes a daemon and starts listening for connections. The standard streams are connected to the terminal from which the server was started. Error and informative messages should be sent to the terminal. WS_EXIT_FUNC() If you have exit handlers you would like to register to run before the server exits, do not invoke atexit() from ws_init_func(). If you do so, your handlers are run when the server becomes a daemon. The server forks to create a new session. The dying parent process will call your handlers when it exits. Instead, call your cleanup code from ws_exit_func(). WS_SET_NAME() Call ws_set_name() inside ws_init_func() to set the server's name. void ws_set_name( char * ); If not set the server's name defaults to "server". The server's name is used in 3 ways: • When the server is running, stderr is connected to /dev/null. Errors must are reported with syslog(3). Dorrit calls openlog() with the server's name as argument to ensure that log entries are identified by the server's name. • The server's pidfile is written to /var/run/ if the server is started as root. The filename is the server's name with ".pid" appended to it. This file is used by rc.d scripts to stop the server. A sample script is included in the dorrit distribution. • The server's listening socket is created in /var/run/ unless you override this with the -l option. If you do not use the -l option, the name defaults to dorrit.socket. With the server's name explicitly set, the name is .socket. WS_SET_PERIODIC() Install a function to be invoked periodically with: void ws_set_periodic( void (*)(), int ); Dorrit calls the function pointed to by first argument when the number of seconds specified by the second argument have elapsed and then again repeatedly when that number of seconds has elapsed since the last call. WS_OPEN_CALLBACK() Dorrit calls ws_open_callback() when a new connection is received. int ws_open_callback( void *, char *cookie ); The first argument is an opaque pointer that uniquely identifies the connection. You pass the pointer to other library functions as necessary. The second argument points to the content of the client's Cookie header line presented in the WebSocket upgrade request. Return 0 from ws_open_callback() to indicate that the connection should be accepted. Return a non-zero value to indicate that the connection should be dropped. If the protocol your server implements requires the server to speak first, send the initial data with ws_write_conn() in ws_open_callback(). WS_SET_DATA() WS_GET_DATA() Use ws_set_data() to associate a void pointer with a connection. void ws_set_data( void *conn, void *data ); The data argument is a pointer of any kind that you want to associate with the connection referenced by the conn argument. Use ws_get_data() to retrieve the stored pointer. void *ws_get_data( void *conn ); WS_GET_QLEN Use ms_get_qlen() to know the number of outgoing frames currently queued for delivery to a client. unsigned int ws_get_qlen( void *conn ); To control memory exhaustion, use this function to detect connections that have gone idle and are not consuming their queues. Close the connections, or stop queueing data for them. WS_CLOSE_CALLBACK() Dorrit calls ws_close_callback() when a connection is closed. void ws_close_callback( void *data ); The function's argument is the opaque pointer identifying the connection. Dorrit frees any outgoing queued data when ws_close_callback() returns. WS_READ_CALLBACK() Dorrit calls ws_read_callback() when a complete WebSocket message has been read from the client. Dorrit unframes and unmasks the payload and passes it to this function. void ws_read_callback( void *, int binary, int len, unsigned char *buffer ); The second argument is a flag indicating if the payload data is binary data. The third argument is the length of the payload data in bytes. The length will never be greater than the maximum buffer size less 5 bytes (defaults to 65535 bytes. See the description of the -b option). The fourth argument is a character pointer to the payload data. This data is always zero-terminated. The terminator is not part of the incoming message and is not counted in the length argument. The fourth argument points into a buffer that will be reused on further reads of client data. If you want to preserve the incoming data when ws_read_callback returns, copy the data to another location. WS_WRITE_CONN() Queue an outgoing message for clients with ws_write_conn(). int ws_write_conn( void **, int conns, int binary, unsigned int len, unsigned char *data ); The first argument is a pointer to an array of opaque pointers representing the connections to which the data will be written. The second argument is the number of elements in the first argument. The third argument is a flag indicating whether or not the content is binary data. If this argument is non-zero, the opcode for the outgoing frames is set to binary (2). Otherwise, the opcode is set to text (1). The fourth argument is the length of the payload data in bytes. The fifth argument is a character pointer to the payload data. The data is copied to an internal buffer and does not have to be zero-terminated. ws_write_conn() does not write the data but frames it and queues the frames for delivery. Payloads greater in size than the maximum buffer size - 5 are broken into fragment frames to facilitate smoother multiplexing. Each time a write event for a connection is generated, the library will send no more than one frame to the client. All queued data is reference counted to avoid unnecessary copying. Each connection's queue of outgoing data points to the same copy of the data. When a connection writes the data or closes, the reference count for the data is decremented. When the reference count becomes zero, the data is freed. ws_write_conn() returns: • -1 on allcation errors. • -2 if len is less than 1. • 0 on success. If the function returns -1, the server cannot allocate memory. There is no point continuing to service this connection. Perform any clean up operations necessary, and call ws_close_conn() to drop the connection. WS_CLOSE_CONN() To close a connection, invoke ws_close_conn(). void ws_close_conn( void *, int force ); ws_close_conn() closes a connection immediately if the second "force" argument is non-zero. If "force" is zero, the connection is closed when the outgoing queue has been written to the client. In that situation, further incoming data is not read from the client. CONFIGURATION Dorrit writes its pidfile into /var/run/ if is started as root. The library is stopped with SIGTERM. A sample control script is provided in the dorrit distribution. To use the script, you must replace all occurrences of "dorrit" with the value you pass to ws_set_name(). The script must be renamed as the value you passed to ws_set_name() and installed in /usr/local/etc/rc.d. Two variables must be added to /etc/rc.conf to use the script. Substitute your server's name for "server": server_enable="YES" server_flags="-u www -g www If the "enable" variable is set to "YES", the server is started at system start. Use the following rc commands: service dorrit start service dorrit stop service dorrit restart service dorrit status If you do not want the server started on system start, then set dorrit_enable="NO" and use the following commands: service dorrit onestart service dorrit onestop service dorrit onerestart service dorrit onestatus COMMAND-LINE OPTIONS The following command line options are recognized by dorrit servers. All of these are optional. -b By default, dorrit uses buffers of up to 65540 bytes to contain WebSocket frames. 65535 is the largest size that can be specified in 16 bits. Dorrit does not support incoming WebSocket frames with larger payloads. 4 bytes are added to the buffer size for the WebSocket header. 1 byte is added for a zero terminator. 65535 + 4 + 1 = 65540. The mask for incoming frames is stored elsewhere. To reduce memory consumption, you can specify a smaller maximum buffer size with the -b option, but you cannot set the maximum buffer size to less than 10 bytes or to more than 65540 bytes. Dorrit drops connections on which messages arrive with payloads greater than the buffer size less 5 bytes. Dorrit drops connections on which fragment frames arrive. Incoming messages must not be fragmented. You can queue outgoing messages of any size, but if messages are greater than the maximum buffer size, the messages are broken into fragment frames of the maximum buffer size or less. -l By default, dorrit listens on /var/run/dorrit.socket. If you set the server's name with ws_set_name(), then "dorrit" in the preceding path is replaced with the name you set. To create the socket, dorrit must start as root. The -l option instructs the library to listen on another socket instead. Specify the fully qualified path to the socket as argument. The server creates the listening socket when it starts, unlinking it first if it already exists in the filesystem. The owner and group of the socket are changed to the values of the -u and -g options. The permisssions of the socket are set to srwxrwx---. -m By default, dorrit maintains no more than 16384 simultaneous connections. The -m option changes this value. -u -g The -u and the -g options are used to specify the user and group for the server. If the server starts as an unprivileged user and group, then these options must be set to the user and group. For dorrit to change to a different user and group, it must be started as root. Both values default to "nobody". -f The -f option takes a filename as argument. Dorrit assigns the filename to the global character pointer ws_config_file. This enables code in ws_init_func() and to access a configuration file. AUTHORS James Bailie ⟨jimmy@mammothcheese.ca⟩ http://www.mammothcheese.ca Fri Sep 09, 2022