jitterbug/server.c
2023-01-03 17:20:02 +02:00

782 lines
27 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <inttypes.h>
#include "match.h"
#include "try.h"
#include "server.h"
#include "wire.h"
#include "util.h"
// https://en.wikipedia.org/wiki/Fowler-Noll-Vo_hash_function#FNV-1a_hash
uint64_t hashmap_hash(const char *bytes, size_t bytes_n, size_t map_len)
{
uint64_t hash = 0xcbf29ce484222325;
for (size_t i = 0; i < bytes_n; i++) {
hash *= 0x100000001b3;
hash ^= bytes[i];
}
return (hash % map_len);
}
int jb_server_client_add(struct jb_server *s, int fd)
{
for (int i = 0; i < JB_MAX_CLIENTS; i++) {
if (s->clients[i].fd < 0) {
s->clients[i].fd = fd;
s->clients[i].owned_name_index = -1;
s->clients[i].unique_name_index = -1;
s->clients[i].state = JB_CLIENT_STATE_WAIT_AUTH;
s->fds[i].fd = fd;
s->fds[i].events = POLLIN;
s->clients_count++;
return i;
}
}
return -1;
}
int jb_server_name_find(struct jb_server *s, char *name)
{
int bucket = hashmap_hash(name, strlen(name), JB_MAX_NAMES);
for (int i = bucket; i < bucket + 12 && i < JB_MAX_NAMES; i++) {
if (s->names[i].name && strcmp(s->names[i].name, name) == 0) {
return i;
}
}
return -1;
}
struct jb_client *jb_server_name_find_client(struct jb_server *s, char *name)
{
int name_index = jb_server_name_find(s, name);
if (name_index < 0) {
return NULL;
}
if (s->names[name_index].client_index < 0) {
return NULL;
}
if (s->clients[s->names[name_index].client_index].state != JB_CLIENT_STATE_READY) {
return NULL;
}
return &s->clients[s->names[name_index].client_index];
}
int jb_server_name_add(struct jb_server *s, char *name, int client_index)
{
if (jb_server_name_find(s, name) >= 0) {
return -1;
}
int bucket = hashmap_hash(name, strlen(name), JB_MAX_NAMES);
for (int i = bucket; i < bucket + 12 && i < JB_MAX_NAMES; i++) {
if (s->names[i].client_index == -1) {
s->names[i].client_index = client_index;
s->names[i].name = name;
s->names_count++;
return i;
}
}
return -1;
}
int jb_server_client_match_add(struct jb_server *s, int client_index, char *match)
{
struct jb_client *c = &s->clients[client_index];
for (int i = 0; i < JB_MAX_MATCH; i++) {
if (!c->matches[i]) {
c->matches[i] = match_rule_from_string(match);
c->match_count++;
return 0;
}
}
return -1;
}
int jb_server_client_assign_unique_name(struct jb_server *s, int i)
{
struct jb_client *c = &s->clients[i];
if (c->unique_name_index != -1) {
return -1;
}
uint32_t id = 0;
FILE *urandom_file = fopen("/dev/urandom", "rb");
if (!urandom_file) {
return -1;
}
if (fread(&id, 1, sizeof(uint32_t), urandom_file) != sizeof(uint32_t)) {
return -1;
}
fclose(urandom_file);
char *name = malloc(sizeof(char) * 16);
if (!name) {
return -1;
}
if (snprintf(name, 16, ":%"PRIu32, id) < 0) {
return -1;
}
int name_index = jb_server_name_add(s, name, i);
if (name_index < 0) {
free(name);
return -1;
}
c->unique_name_index = name_index;
return name_index;
}
int jb_server_client_assign_own_name(struct jb_server *s, int i, char *name)
{
struct jb_client *c = &s->clients[i];
if (c->owned_name_index != -1 || !name || *name == ':' || *name == '\0') {
return -1;
}
int name_index = jb_server_name_add(s, name, i);
if (name_index < 0) {
return -1;
}
c->owned_name_index = name_index;
return 0;
}
void jb_server_name_remove(struct jb_server *s, int i)
{
if (i >= 0) {
if (s->names[i].name) {
free(s->names[i].name);
s->names[i].name = NULL;
}
s->names[i].client_index = -1;
s->names_count--;
}
}
void jb_server_client_remove(struct jb_server *s, int i)
{
struct jb_client *c = &s->clients[i];
if (c->fd >= 0) {
close(c->fd);
}
for (int i = 0; i < JB_MAX_MATCH; i++) {
if (c->matches[i]) {
match_rule_free(c->matches[i]);
c->matches[i] = NULL;
}
}
jb_server_name_remove(s, c->unique_name_index);
jb_server_name_remove(s, c->owned_name_index);
c->unique_name_index = -1;
c->owned_name_index = -1;
c->match_count = 0;
c->fd = -1;
c->state = JB_CLIENT_STATE_NONE;
s->fds[i].fd = -1;
s->fds[i].events = 0;
s->fds[i].revents = 0;
s->clients_count--;
}
void jb_server_client_error(struct jb_server *s, int i, const char *msg)
{
printf("jb_server_client_error: %s\n", msg);
send(s->clients[i].fd, msg, strlen(msg), 0);
jb_server_client_remove(s, i);
}
ssize_t jb_server_client_recv(struct jb_server *s, int i, void *buf, size_t n)
{
ssize_t status = recv(s->fds[i].fd, buf, n, 0);
if (status <= 0) {
jb_server_client_remove(s, i);
}
return status;
}
int jb_server_broadcast_message(struct jb_server *s, wire_message_t *msg, wire_context_t *ctx, char *sender_unique_name)
{
int left = s->clients_count;
for (int i = 0; i < JB_MAX_CLIENTS && left > 0; i++) {
if (s->clients[i].match_count <= 0) {
continue;
}
left--;
struct jb_client *c = &s->clients[i];
int match_left = c->match_count;
for (int i = 0; i < JB_MAX_MATCH && match_left > 0; i++) {
if (!c->matches[i]) {
continue;
}
match_left--;
if (match_rule_check(c->matches[i], msg, ctx) >= 0) {
uint8_t reply_data[4096];
memset(reply_data, 0, 4096);
wire_context_t reply_ctx = {
.byte_cursor = 0,
.data = reply_data,
.data_len = 4096,
};
TRYST(wire_compose_unicast_reply(&reply_ctx, ctx, msg, sender_unique_name));
TRYST(send(c->fd, reply_data, reply_ctx.byte_cursor, 0));
}
}
}
return 0;
}
int jb_server_unicast_message(struct jb_server *s, wire_message_t *msg, wire_context_t *ctx, char *target_name, char *sender_unique_name)
{
struct jb_client *target = jb_server_name_find_client(s, target_name);
if (!target) {
return -1;
}
uint8_t reply_data[4096];
memset(reply_data, 0, 4096);
wire_context_t reply_ctx = {
.byte_cursor = 0,
.data = reply_data,
.data_len = 4096,
};
TRYST(wire_compose_unicast_reply(&reply_ctx, ctx, msg, sender_unique_name));
TRYST(send(target->fd, reply_data, reply_ctx.byte_cursor, 0));
return 0;
}
#define _reply_begin(M_sig) \
if (!(msg.flags & DBUS_FLAG_NO_REPLY_EXPECTED)) { \
TRYST(wire_compose_reply(&reply_ctx, &msg, (M_sig), &body_length)); \
body_start = reply_ctx.byte_cursor; \
#define _reply_end() \
*body_length = reply_ctx.byte_cursor - body_start; \
if (send(s->fds[i].fd, reply_data, reply_ctx.byte_cursor, 0) != reply_ctx.byte_cursor) { \
return -1; \
} \
printf("send: sent %d bytes!\n", reply_ctx.byte_cursor); \
} /* if (!(msg.flags & DBUS_FLAG_NO_REPLY_EXPECTED)) */
#define _reply_error(message) \
do { \
if (!(msg.flags & DBUS_FLAG_NO_REPLY_EXPECTED)) { \
TRYST(wire_compose_error(&reply_ctx, &msg, (message))); \
if (send(s->fds[i].fd, reply_data, reply_ctx.byte_cursor, 0) != reply_ctx.byte_cursor) { \
return -1; \
} \
} \
} while(0) \
int jb_server_client_process_message(struct jb_server *s, int i, uint8_t *data, size_t data_len)
{
struct jb_client *client = &s->clients[i];
wire_message_t msg = {0};
wire_context_t ctx = {
.byte_cursor = 0,
.data = data,
.data_len = data_len,
};
if (wire_parse_message(&ctx, &msg) < 0) {
printf("parsing failed\n");
return -1;
}
printf("process_message: processed %d/%zu bytes\n", ctx.byte_cursor, data_len);
wire_message_field_t *destination_field = &msg.fields[DBUS_HEADER_FIELD_DESTINATION];
wire_message_field_t *member_field = &msg.fields[DBUS_HEADER_FIELD_MEMBER];
bool should_forward = false;
uint32_t *body_length;
uint32_t body_start;
uint8_t reply_data[4096];
memset(reply_data, 0, 4096);
wire_context_t reply_ctx = {
.byte_cursor = 0,
.data = reply_data,
.data_len = 4096,
};
switch (msg.type) {
case DBUS_MESSAGE_METHOD_CALL: {
if (!member_field->present) {
return -1;
}
if (!destination_field->present) {
should_forward = true;
break;
}
char *member = member_field->t.str;
if (strcmp(destination_field->t.str, "org.freedesktop.DBus") != 0) {
// not for dbus.
should_forward = true;
break;
}
if (strcmp(member, "Hello") == 0) {
int unique_name_index = jb_server_client_assign_unique_name(s, i);
if (unique_name_index < 0) {
return -1;
}
_reply_begin("s") {
TRYPTR(wire_set_string(&reply_ctx, s->names[unique_name_index].name));
} _reply_end()
printf("assigned unique name '%s' to connection %d\n", s->names[unique_name_index].name, i);
} else if (strcmp(member, "RequestName") == 0) {
char *name = TRYPTR(wire_get_string(&ctx));
int name_len = strlen(name);
if (name_len < 1 || name_len > 256) {
return -1;
}
char *name_str = TRYPTR(string_dup(name));
int status_code = DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER;
if (jb_server_client_assign_own_name(s, i, name_str) < 0) {
free(name_str);
// TODO: report the actual error
status_code = DBUS_REQUEST_NAME_REPLY_EXISTS;
}
_reply_begin("u") {
TRYPTR(wire_set_u32(&reply_ctx, status_code));
} _reply_end()
if (status_code == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
printf("client '%s' (index=%d) now owns name '%s'\n", s->names[client->unique_name_index].name, i, name_str);
}
} else if (strcmp(member, "GetNameOwner") == 0) {
char *name = TRYPTR(wire_get_string(&ctx));
int name_len = strlen(name);
if (name_len < 1 || name_len > 256) {
return -1;
}
struct jb_client *target = jb_server_name_find_client(s, name);
if (!target || target->unique_name_index < 0) {
_reply_error("org.freedesktop.DBus.Error.NameHasNoOwner");
return 0;
}
_reply_begin("s") {
TRYPTR(wire_set_string(&reply_ctx, s->names[target->unique_name_index].name));
} _reply_end()
} else if (strcmp(member, "NameHasOwner") == 0) {
char *name = TRYPTR(wire_get_string(&ctx));
int name_len = strlen(name);
if (name_len < 1 || name_len > 256) {
return -1;
}
struct jb_client *target = jb_server_name_find_client(s, name);
_reply_begin("b") {
TRYPTR(wire_set_u32(&reply_ctx, target ? 1 : 0));
} _reply_end()
} else if (strcmp(member, "ListNames") == 0) {
_reply_begin("as") {
uint32_t *array_length = TRYPTR(wire_set_u32(&reply_ctx, 0));
/* arrays start with the alignment of the type they contain */
TRYPTR(wire_write_align(&reply_ctx, 4));
uint32_t array_start = reply_ctx.byte_cursor;
TRYPTR(wire_set_string(&reply_ctx, "org.freedesktop.DBus"));
int left = s->names_count;
for (int i = 0; i < JB_MAX_NAMES && left > 0; i++) {
if (s->names[i].name && s->names[i].client_index >= 0) {
left--;
TRYPTR(wire_set_string(&reply_ctx, s->names[i].name));
}
}
*array_length = reply_ctx.byte_cursor - array_start;
} _reply_end()
} else if (strcmp(member, "ListActivatableNames") == 0) {
// TODO: stub (always returns empty array)
printf("FIXME: STUB: ListActivatableNames\n");
_reply_begin("as") {
TRYPTR(wire_set_u32(&reply_ctx, 0));
// empty arrays still need to align
TRYPTR(wire_write_align(&reply_ctx, 4));
} _reply_end()
} else if (strcmp(member, "StartServiceByName") == 0) {
// TODO: stub (does nothing and always returns success)
char *name = TRYPTR(wire_get_string(&ctx));
int name_len = strlen(name);
if (name_len < 1 || name_len > 256) {
return -1;
}
/* unused flags value */
TRYPTR(wire_get_u32(&ctx));
printf("FIXME: STUB: StartServiceByName: %s\n", name);
_reply_begin("u") {
TRYPTR(wire_set_u32(&reply_ctx, 1));
} _reply_end()
} else if (strcmp(member, "GetConnectionUnixProcessID") == 0) {
// TODO: stub (returns an error)
char *name = TRYPTR(wire_get_string(&ctx));
int name_len = strlen(name);
if (name_len < 1 || name_len > 256) {
return -1;
}
printf("FIXME: STUB: GetConnectionUnixProcessID: %s\n", name);
_reply_error("xyz.hippoz.jitterbug.NotImplemented");
} else if (strcmp(member, "AddMatch") == 0) {
char *match = TRYPTR(wire_get_string(&ctx));
int match_len = strlen(match);
if (match_len < 1 || match_len > MATCH_RULE_MAX) {
return -1;
}
printf("client index %d adding match rule: '%s'\n", i, match);
TRYST(jb_server_client_match_add(s, i, match));
_reply_begin("") {} _reply_end()
} else if (strcmp(member, "RemoveMatch") == 0) {
// TODO: stub (does nothing and always returns success)
char *match = TRYPTR(wire_get_string(&ctx));
int match_len = strlen(match);
if (match_len < 1 || match_len > MATCH_RULE_MAX) {
return -1;
}
printf("FIXME: STUB: RemoveMatch: %s\n", match);
_reply_begin("") {} _reply_end()
} else if (strcmp(member, "GetAllMatchRules") == 0) {
_reply_begin("a{sas}") {
uint32_t *outer_array_len = TRYPTR(wire_set_u32(&reply_ctx, 0));
TRYPTR(wire_write_align(&reply_ctx, 8)); /* arrays start with the alignment of the type they contain */
uint32_t outer_array_start = reply_ctx.byte_cursor;
int left = s->names_count;
for (int i = 0; i < JB_MAX_NAMES && left > 0; i++) {
struct jb_name *n = &s->names[i];
if (!n->name || n->client_index < 0 || *n->name != ':') {
continue;
}
left--;
TRYPTR(wire_write_align(&reply_ctx, 8)); /* structs always aligned to 8 */
struct jb_client *c = &s->clients[s->names[i].client_index];
TRYPTR(wire_set_string(&reply_ctx, s->names[i].name)); /* unique name */
/* array of all match rules */
int match_left = c->match_count;
uint32_t *name_array_len = TRYPTR(wire_set_u32(&reply_ctx, 0));
TRYPTR(wire_write_align(&reply_ctx, 4)); /* arrays start with the alignment of the type they contain */
uint32_t name_array_start = reply_ctx.byte_cursor;
for (int i = 0; i < JB_MAX_MATCH && match_left > 0; i++) {
if (!c->matches[i]) {
continue;
}
match_left--;
TRYPTR(wire_set_string(&reply_ctx, c->matches[i]->rule_string));
}
*name_array_len = reply_ctx.byte_cursor - name_array_start;
}
*outer_array_len = reply_ctx.byte_cursor - outer_array_start;
} _reply_end()
} else {
printf("FIXME: daemon method '%s' is not implemented or invalid\n", member);
_reply_error("org.freedesktop.DBus.Error.UnknownMethod");
return 0;
}
} break;
case DBUS_MESSAGE_METHOD_RETURN: {
if (!destination_field->present) {
return -1;
}
should_forward = true;
} break;
case DBUS_MESSAGE_SIGNAL: {
should_forward = true;
} break;
default: {
_reply_error("xyz.hippoz.jitterbug.NotImplemented");
return 0;
} break;
}
if (should_forward) {
if (client->unique_name_index < 0) {
return -1;
}
if (destination_field->present) {
/* unicast */
if (jb_server_unicast_message(s, &msg, &ctx, destination_field->t.str, s->names[client->unique_name_index].name) < 0) {
_reply_error("xyz.hippoz.jitterbug.UnicastFailed");
return 0;
}
} else {
/* broadcast */
if (jb_server_broadcast_message(s, &msg, &ctx, s->names[client->unique_name_index].name) < 0) {
_reply_error("xyz.hippoz.jitterbug.BroadcastFailed");
return 0;
}
}
}
if (ctx.byte_cursor < ctx.data_len && ctx.data[ctx.byte_cursor] == 'l') {
// another message
TRYST(jb_server_client_process_message(s, i, ctx.data + ctx.byte_cursor, ctx.data_len - ctx.byte_cursor));
}
return 0;
}
void jb_server_free(struct jb_server *s)
{
if (!s) return;
if (s->sock_fd) close(s->sock_fd);
// freeing all clients will also free all matches and names
for (int i = 0; i < JB_MAX_CLIENTS; i++) {
jb_server_client_remove(s, i);
}
free(s);
}
struct jb_server *jb_server_create(const char *socket_path)
{
struct jb_server *s = malloc(sizeof(struct jb_server));
if (s == NULL) {
return NULL;
}
s->sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (s->sock_fd == -1) {
free(s);
return NULL;
}
struct sockaddr_un name;
memset(&name, 0, sizeof(name));
name.sun_family = AF_UNIX;
strncpy(name.sun_path, socket_path, sizeof(name.sun_path) - 1);
unlink(socket_path);
if (bind(s->sock_fd, (const struct sockaddr *)&name, sizeof(name)) == -1) {
close(s->sock_fd);
free(s);
return NULL;
}
if (listen(s->sock_fd, JB_BACKLOG) == -1) {
close(s->sock_fd);
free(s);
return NULL;
}
for (int i = 0; i < JB_MAX_CLIENTS; i++) {
s->clients[i].fd = -1;
s->clients[i].owned_name_index = -1;
s->clients[i].unique_name_index = -1;
s->clients[i].state = JB_CLIENT_STATE_NONE;
s->clients[i].match_count = 0;
s->fds[i].fd = -1;
s->fds[i].events = 0;
s->fds[i].revents = 0;
for (int j = 0; j < JB_MAX_MATCH; j++) {
s->clients[i].matches[j] = NULL;
}
}
for (int i = 0; i < JB_MAX_NAMES; i++) {
s->names[i].client_index = -1;
s->names[i].name = NULL;
}
s->names_count = 0;
s->clients_count = 0;
s->fds[JB_MAX_CLIENTS].fd = s->sock_fd;
s->fds[JB_MAX_CLIENTS].events = POLLIN;
s->fd_num = JB_MAX_CLIENTS + 1;
return s;
}
#define _client_die(m) ({jb_server_client_error(s,i,m);continue;})
int jb_server_turn(struct jb_server *s)
{
static const char agree_unix_fd[] = "AGREE_UNIX_FD\r\n";
static const char auth_ok[] = "OK aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n";
static const char auth_list[] = "REJECTED EXTERNAL\r\n";
static const char auth_data[] = "DATA\r\n";
static const int data_buffer_len = 4096;
// We can keep a padding of null bytes for the data buffer. In the event that, for
// example, a string function is called on a char array without a proper ending null
// byte, we will reach the null bytes here at the end instead, thus preventing a
// crash and potential corruption. While this is a good "last line of defense"
// against such issues, it is much more important for these kinds of bugs to not
// exist in the first place. This mitigation will prevent, for example, ASAN from
// finding such bugs. It's recommended that you disable this padding outside
// of production so that you can find these bugs.
static const int data_buffer_padding = 0;
TRYST(poll(s->fds, s->fd_num, -1));
for (int i = 0; i < s->fd_num; i++) {
int fd = s->fds[i].fd;
if (s->fds[i].revents & POLLNVAL) {
fprintf(stderr, "jb_server_turn: error: got POLLNVAL for fds[%d]. This is considered a bug in the server implementation.\n", i);
return -1;
}
if (s->fds[i].revents & POLLERR) {
if (fd == s->sock_fd) {
fprintf(stderr, "jb_server_turn: error: got POLLERR for sock_fd\n");
return -1;
}
jb_server_client_remove(s, i);
continue;
}
if (s->fds[i].revents & POLLHUP) {
if (fd == s->sock_fd) {
fprintf(stderr, "jb_server_turn: error: got POLLHUP for sock_fd\n");
return -1;
}
jb_server_client_remove(s, i);
continue;
}
if (s->fds[i].revents & POLLIN) {
// file descriptor ready for reading
if (fd == s->sock_fd) {
// new connection
int fd = TRYST(accept(s->sock_fd, NULL, NULL));
if (jb_server_client_add(s, fd) == -1) {
close(fd);
}
continue;
}
// We add padding. See above.
char data[data_buffer_len + data_buffer_padding];
memset(data, 0, sizeof(data));
ssize_t bytes = recv(fd, data, data_buffer_len, 0);
if (bytes <= 0) {
// error during recv() OR client disconnected, disconnect the client
// TODO: should we actually do this?
jb_server_client_remove(s, i);
continue;
}
printf("\nrecv: got %zd bytes\n", bytes);
struct jb_client *c = &s->clients[i];
switch (c->state) {
case JB_CLIENT_STATE_WAIT_AUTH: {
// The D-Bus authentication protocol is a simple plain text protocol.
// Before the flow of messages can begin, the two applications must authenticate.
// SPEC: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
// Immediately after connecting, clients must send a null byte
// SPEC: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-nul-byte
char *auth_string = data;
if (*auth_string == '\0') {
auth_string++;
}
// TODO: the code below is hacky and does not reflect the specification
if (strcmp(auth_string, "AUTH\r\n") == 0) {
send(fd, auth_list, sizeof(auth_list) - 1, 0);
} else if (strcmp(auth_string, "AUTH EXTERNAL 31303030\r\n") == 0 || strcmp(auth_string, "DATA\r\n") == 0) {
send(fd, auth_ok, sizeof(auth_ok) - 1, 0);
c->state = JB_CLIENT_STATE_WAIT_BEGIN;
} else if (strcmp(auth_string, "AUTH EXTERNAL\r\n") == 0) {
send(fd, auth_data, sizeof(auth_data) - 1, 0);
}
} break;
case JB_CLIENT_STATE_WAIT_BEGIN: {
// Right now, we're expecting the client to either immediately begin the connection,
// or to negotiate UNIX file descriptor passing.
if (strncmp(data, "BEGIN\r\n", 7) == 0) {
c->state = JB_CLIENT_STATE_READY;
// At this point, a D-Bus connection has been established.
// The first octet after the \r\n of the BEGIN command is the first octet of the D-Bus communication.
// SPEC: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-command-begin
char *first_message_begin = data + 7; /* 7 = length of "BEGIN\r\n" */
if (*first_message_begin == 'l' || *first_message_begin == 'B') {
// This looks like a D-Bus message. Let's process it!
if (jb_server_client_process_message(s, i, (uint8_t *)first_message_begin, data_buffer_len - 7) < 0) {
_client_die("failed to process message");
}
}
} else if (strcmp(data, "NEGOTIATE_UNIX_FD\r\n") == 0) {
// SPEC: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-command-negotiate-unix-fd
send(fd, agree_unix_fd, sizeof(agree_unix_fd) - 1, 0);
} else {
_client_die("bad auth response (expected BEGIN or NEGOTIATE_UNIX_FD)");
}
} break;
case JB_CLIENT_STATE_READY: {
if (jb_server_client_process_message(s, i, (uint8_t*)data, data_buffer_len) < 0) {
_client_die("failed to process message");
}
} break;
case JB_CLIENT_STATE_NONE: {} /* through */
default: {
_client_die("bad state");
} break;
}
}
}
return 0;
}