| /* |
| * Copyright (C) 2014 Martin Willi |
| * Copyright (C) 2014 revosec AG |
| * |
| * 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 2 of the License, or (at your |
| * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. |
| * |
| * 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. |
| */ |
| |
| #include "vici_socket.h" |
| |
| #include <threading/mutex.h> |
| #include <threading/condvar.h> |
| #include <threading/thread.h> |
| #include <collections/array.h> |
| #include <collections/linked_list.h> |
| #include <processing/jobs/callback_job.h> |
| |
| #include <errno.h> |
| #include <string.h> |
| |
| typedef struct private_vici_socket_t private_vici_socket_t; |
| |
| /** |
| * Private members of vici_socket_t |
| */ |
| struct private_vici_socket_t { |
| |
| /** |
| * public functions |
| */ |
| vici_socket_t public; |
| |
| /** |
| * Inbound message callback |
| */ |
| vici_inbound_cb_t inbound; |
| |
| /** |
| * Client connect callback |
| */ |
| vici_connect_cb_t connect; |
| |
| /** |
| * Client disconnect callback |
| */ |
| vici_disconnect_cb_t disconnect; |
| |
| /** |
| * Next client connection identifier |
| */ |
| u_int nextid; |
| |
| /** |
| * User data for callbacks |
| */ |
| void *user; |
| |
| /** |
| * Service accepting vici connections |
| */ |
| stream_service_t *service; |
| |
| /** |
| * Client connections, as entry_t |
| */ |
| linked_list_t *connections; |
| |
| /** |
| * mutex for client connections |
| */ |
| mutex_t *mutex; |
| }; |
| |
| /** |
| * Data to securely reference an entry |
| */ |
| typedef struct { |
| /* reference to socket instance */ |
| private_vici_socket_t *this; |
| /** connection identifier of entry */ |
| u_int id; |
| } entry_selector_t; |
| |
| /** |
| * Partially processed message |
| */ |
| typedef struct { |
| /** bytes of length header sent/received */ |
| u_char hdrlen; |
| /** bytes of length header */ |
| char hdr[sizeof(uint32_t)]; |
| /** send/receive buffer on heap */ |
| chunk_t buf; |
| /** bytes sent/received in buffer */ |
| uint32_t done; |
| } msg_buf_t; |
| |
| /** |
| * Client connection entry |
| */ |
| typedef struct { |
| /** reference to socket */ |
| private_vici_socket_t *this; |
| /** associated stream */ |
| stream_t *stream; |
| /** queued messages to send, as msg_buf_t pointers */ |
| array_t *out; |
| /** input message buffer */ |
| msg_buf_t in; |
| /** queued input messages to process, as chunk_t */ |
| array_t *queue; |
| /** do we have job processing input queue? */ |
| bool has_processor; |
| /** is this client disconnecting */ |
| bool disconnecting; |
| /** client connection identifier */ |
| u_int id; |
| /** any users reading over this connection? */ |
| int readers; |
| /** any users writing over this connection? */ |
| int writers; |
| /** condvar to wait for usage */ |
| condvar_t *cond; |
| } entry_t; |
| |
| /** |
| * Destroy an connection entry |
| */ |
| CALLBACK(destroy_entry, void, |
| entry_t *entry) |
| { |
| msg_buf_t *out; |
| chunk_t chunk; |
| |
| entry->stream->destroy(entry->stream); |
| entry->this->disconnect(entry->this->user, entry->id); |
| entry->cond->destroy(entry->cond); |
| |
| while (array_remove(entry->out, ARRAY_TAIL, &out)) |
| { |
| chunk_clear(&out->buf); |
| free(out); |
| } |
| array_destroy(entry->out); |
| while (array_remove(entry->queue, ARRAY_TAIL, &chunk)) |
| { |
| chunk_clear(&chunk); |
| } |
| array_destroy(entry->queue); |
| chunk_clear(&entry->in.buf); |
| free(entry); |
| } |
| |
| /** |
| * Find entry by stream (if given) or id, claim use |
| */ |
| static entry_t* find_entry(private_vici_socket_t *this, stream_t *stream, |
| u_int id, bool reader, bool writer) |
| { |
| enumerator_t *enumerator; |
| entry_t *entry, *found = NULL; |
| bool candidate = TRUE; |
| |
| this->mutex->lock(this->mutex); |
| while (candidate && !found) |
| { |
| candidate = FALSE; |
| enumerator = this->connections->create_enumerator(this->connections); |
| while (enumerator->enumerate(enumerator, &entry)) |
| { |
| if (stream) |
| { |
| if (entry->stream != stream) |
| { |
| continue; |
| } |
| } |
| else |
| { |
| if (entry->id != id) |
| { |
| continue; |
| } |
| } |
| if (entry->disconnecting) |
| { |
| entry->cond->signal(entry->cond); |
| continue; |
| } |
| candidate = TRUE; |
| |
| if ((reader && entry->readers) || |
| (writer && entry->writers)) |
| { |
| entry->cond->wait(entry->cond, this->mutex); |
| break; |
| } |
| if (reader) |
| { |
| entry->readers++; |
| } |
| if (writer) |
| { |
| entry->writers++; |
| } |
| found = entry; |
| break; |
| } |
| enumerator->destroy(enumerator); |
| } |
| this->mutex->unlock(this->mutex); |
| |
| return found; |
| } |
| |
| /** |
| * Remove entry by id, claim use |
| */ |
| static entry_t* remove_entry(private_vici_socket_t *this, u_int id) |
| { |
| enumerator_t *enumerator; |
| entry_t *entry, *found = NULL; |
| bool candidate = TRUE; |
| |
| this->mutex->lock(this->mutex); |
| while (candidate && !found) |
| { |
| candidate = FALSE; |
| enumerator = this->connections->create_enumerator(this->connections); |
| while (enumerator->enumerate(enumerator, &entry)) |
| { |
| if (entry->id == id) |
| { |
| candidate = TRUE; |
| if (entry->readers || entry->writers) |
| { |
| entry->cond->wait(entry->cond, this->mutex); |
| break; |
| } |
| this->connections->remove_at(this->connections, enumerator); |
| entry->cond->broadcast(entry->cond); |
| found = entry; |
| break; |
| } |
| } |
| enumerator->destroy(enumerator); |
| } |
| this->mutex->unlock(this->mutex); |
| |
| return found; |
| } |
| |
| /** |
| * Release a claimed entry |
| */ |
| static void put_entry(private_vici_socket_t *this, entry_t *entry, |
| bool reader, bool writer) |
| { |
| this->mutex->lock(this->mutex); |
| if (reader) |
| { |
| entry->readers--; |
| } |
| if (writer) |
| { |
| entry->writers--; |
| } |
| entry->cond->signal(entry->cond); |
| this->mutex->unlock(this->mutex); |
| } |
| |
| /** |
| * Asynchronous callback to disconnect client |
| */ |
| CALLBACK(disconnect_async, job_requeue_t, |
| entry_selector_t *sel) |
| { |
| entry_t *entry; |
| |
| entry = remove_entry(sel->this, sel->id); |
| if (entry) |
| { |
| destroy_entry(entry); |
| } |
| return JOB_REQUEUE_NONE; |
| } |
| |
| /** |
| * Disconnect a connected client |
| */ |
| static void disconnect(private_vici_socket_t *this, u_int id) |
| { |
| entry_selector_t *sel; |
| |
| INIT(sel, |
| .this = this, |
| .id = id, |
| ); |
| |
| lib->processor->queue_job(lib->processor, |
| (job_t*)callback_job_create(disconnect_async, sel, free, NULL)); |
| } |
| |
| /** |
| * Write queued output data |
| */ |
| static bool do_write(private_vici_socket_t *this, entry_t *entry, |
| stream_t *stream, char *errmsg, size_t errlen, bool block) |
| { |
| msg_buf_t *out; |
| ssize_t len; |
| |
| while (array_get(entry->out, ARRAY_HEAD, &out)) |
| { |
| /* write header */ |
| while (out->hdrlen < sizeof(out->hdr)) |
| { |
| len = stream->write(stream, out->hdr + out->hdrlen, |
| sizeof(out->hdr) - out->hdrlen, block); |
| if (len == 0) |
| { |
| return FALSE; |
| } |
| if (len < 0) |
| { |
| if (errno == EWOULDBLOCK) |
| { |
| return TRUE; |
| } |
| snprintf(errmsg, errlen, "vici header write error: %s", |
| strerror(errno)); |
| return FALSE; |
| } |
| out->hdrlen += len; |
| } |
| |
| /* write buffer buffer */ |
| while (out->buf.len > out->done) |
| { |
| len = stream->write(stream, out->buf.ptr + out->done, |
| out->buf.len - out->done, block); |
| if (len == 0) |
| { |
| snprintf(errmsg, errlen, "premature vici disconnect"); |
| return FALSE; |
| } |
| if (len < 0) |
| { |
| if (errno == EWOULDBLOCK) |
| { |
| return TRUE; |
| } |
| snprintf(errmsg, errlen, "vici write error: %s", strerror(errno)); |
| return FALSE; |
| } |
| out->done += len; |
| } |
| |
| if (array_remove(entry->out, ARRAY_HEAD, &out)) |
| { |
| chunk_clear(&out->buf); |
| free(out); |
| } |
| } |
| return TRUE; |
| } |
| |
| /** |
| * Send pending messages |
| */ |
| CALLBACK(on_write, bool, |
| private_vici_socket_t *this, stream_t *stream) |
| { |
| char errmsg[256] = ""; |
| entry_t *entry; |
| bool ret = FALSE; |
| |
| entry = find_entry(this, stream, 0, FALSE, TRUE); |
| if (entry) |
| { |
| ret = do_write(this, entry, stream, errmsg, sizeof(errmsg), FALSE); |
| if (ret) |
| { |
| /* unregister if we have no more messages to send */ |
| ret = array_count(entry->out) != 0; |
| } |
| else |
| { |
| entry->disconnecting = TRUE; |
| disconnect(entry->this, entry->id); |
| } |
| put_entry(this, entry, FALSE, TRUE); |
| |
| if (!ret && errmsg[0]) |
| { |
| DBG1(DBG_CFG, errmsg); |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Read in available header with data, non-blocking accumulating to buffer |
| */ |
| static bool do_read(private_vici_socket_t *this, entry_t *entry, |
| stream_t *stream, char *errmsg, size_t errlen) |
| { |
| uint32_t msglen; |
| ssize_t len; |
| |
| /* assemble the length header first */ |
| while (entry->in.hdrlen < sizeof(entry->in.hdr)) |
| { |
| len = stream->read(stream, entry->in.hdr + entry->in.hdrlen, |
| sizeof(entry->in.hdr) - entry->in.hdrlen, FALSE); |
| if (len == 0) |
| { |
| return FALSE; |
| } |
| if (len < 0) |
| { |
| if (errno == EWOULDBLOCK) |
| { |
| return TRUE; |
| } |
| snprintf(errmsg, errlen, "vici header read error: %s", |
| strerror(errno)); |
| return FALSE; |
| } |
| entry->in.hdrlen += len; |
| if (entry->in.hdrlen == sizeof(entry->in.hdr)) |
| { |
| msglen = untoh32(entry->in.hdr); |
| if (msglen > VICI_MESSAGE_SIZE_MAX) |
| { |
| snprintf(errmsg, errlen, "vici message length %u exceeds %u " |
| "bytes limit, ignored", msglen, VICI_MESSAGE_SIZE_MAX); |
| return FALSE; |
| } |
| /* header complete, continue with data */ |
| entry->in.buf = chunk_alloc(msglen); |
| } |
| } |
| |
| /* assemble buffer */ |
| while (entry->in.buf.len > entry->in.done) |
| { |
| len = stream->read(stream, entry->in.buf.ptr + entry->in.done, |
| entry->in.buf.len - entry->in.done, FALSE); |
| if (len == 0) |
| { |
| snprintf(errmsg, errlen, "premature vici disconnect"); |
| return FALSE; |
| } |
| if (len < 0) |
| { |
| if (errno == EWOULDBLOCK) |
| { |
| return TRUE; |
| } |
| snprintf(errmsg, errlen, "vici read error: %s", strerror(errno)); |
| return FALSE; |
| } |
| entry->in.done += len; |
| } |
| |
| return TRUE; |
| } |
| |
| /** |
| * Callback processing incoming requests in strict order |
| */ |
| CALLBACK(process_queue, job_requeue_t, |
| entry_selector_t *sel) |
| { |
| entry_t *entry; |
| chunk_t chunk; |
| bool found; |
| u_int id; |
| |
| while (TRUE) |
| { |
| entry = find_entry(sel->this, NULL, sel->id, TRUE, FALSE); |
| if (!entry) |
| { |
| break; |
| } |
| |
| found = array_remove(entry->queue, ARRAY_HEAD, &chunk); |
| if (!found) |
| { |
| entry->has_processor = FALSE; |
| } |
| id = entry->id; |
| put_entry(sel->this, entry, TRUE, FALSE); |
| if (!found) |
| { |
| break; |
| } |
| |
| thread_cleanup_push((void*)chunk_clear, &chunk); |
| sel->this->inbound(sel->this->user, id, chunk); |
| thread_cleanup_pop(TRUE); |
| } |
| return JOB_REQUEUE_NONE; |
| } |
| |
| /** |
| * Process incoming messages |
| */ |
| CALLBACK(on_read, bool, |
| private_vici_socket_t *this, stream_t *stream) |
| { |
| char errmsg[256] = ""; |
| entry_selector_t *sel; |
| entry_t *entry; |
| bool ret = FALSE; |
| |
| entry = find_entry(this, stream, 0, TRUE, FALSE); |
| if (entry) |
| { |
| ret = do_read(this, entry, stream, errmsg, sizeof(errmsg)); |
| if (!ret) |
| { |
| entry->disconnecting = TRUE; |
| disconnect(this, entry->id); |
| } |
| else if (entry->in.hdrlen == sizeof(entry->in.hdr) && |
| entry->in.buf.len == entry->in.done) |
| { |
| array_insert(entry->queue, ARRAY_TAIL, &entry->in.buf); |
| entry->in.buf = chunk_empty; |
| entry->in.hdrlen = entry->in.done = 0; |
| |
| if (!entry->has_processor) |
| { |
| INIT(sel, |
| .this = this, |
| .id = entry->id, |
| ); |
| lib->processor->queue_job(lib->processor, |
| (job_t*)callback_job_create(process_queue, |
| sel, free, NULL)); |
| entry->has_processor = TRUE; |
| } |
| } |
| put_entry(this, entry, TRUE, FALSE); |
| |
| if (!ret && errmsg[0]) |
| { |
| DBG1(DBG_CFG, errmsg); |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Process connection request |
| */ |
| CALLBACK(on_accept, bool, |
| private_vici_socket_t *this, stream_t *stream) |
| { |
| entry_t *entry; |
| u_int id; |
| |
| id = ref_get(&this->nextid); |
| |
| INIT(entry, |
| .this = this, |
| .stream = stream, |
| .id = id, |
| .out = array_create(0, 0), |
| .queue = array_create(sizeof(chunk_t), 0), |
| .cond = condvar_create(CONDVAR_TYPE_DEFAULT), |
| .readers = 1, |
| ); |
| |
| this->mutex->lock(this->mutex); |
| this->connections->insert_last(this->connections, entry); |
| this->mutex->unlock(this->mutex); |
| |
| stream->on_read(stream, on_read, this); |
| |
| put_entry(this, entry, TRUE, FALSE); |
| |
| this->connect(this->user, id); |
| |
| return TRUE; |
| } |
| |
| /** |
| * Async callback to enable writer |
| */ |
| CALLBACK(enable_writer, job_requeue_t, |
| entry_selector_t *sel) |
| { |
| entry_t *entry; |
| |
| entry = find_entry(sel->this, NULL, sel->id, FALSE, TRUE); |
| if (entry) |
| { |
| entry->stream->on_write(entry->stream, on_write, sel->this); |
| put_entry(sel->this, entry, FALSE, TRUE); |
| } |
| return JOB_REQUEUE_NONE; |
| } |
| |
| METHOD(vici_socket_t, send_, void, |
| private_vici_socket_t *this, u_int id, chunk_t msg) |
| { |
| if (msg.len <= VICI_MESSAGE_SIZE_MAX) |
| { |
| entry_selector_t *sel; |
| msg_buf_t *out; |
| entry_t *entry; |
| |
| entry = find_entry(this, NULL, id, FALSE, TRUE); |
| if (entry) |
| { |
| INIT(out, |
| .buf = msg, |
| ); |
| htoun32(out->hdr, msg.len); |
| |
| array_insert(entry->out, ARRAY_TAIL, out); |
| if (array_count(entry->out) == 1) |
| { /* asynchronously re-enable on_write callback when we get data */ |
| INIT(sel, |
| .this = this, |
| .id = entry->id, |
| ); |
| lib->processor->queue_job(lib->processor, |
| (job_t*)callback_job_create(enable_writer, |
| sel, free, NULL)); |
| } |
| put_entry(this, entry, FALSE, TRUE); |
| } |
| else |
| { |
| DBG1(DBG_CFG, "vici connection %u unknown", id); |
| chunk_clear(&msg); |
| } |
| } |
| else |
| { |
| DBG1(DBG_CFG, "vici message size %zu exceeds maximum size of %u, " |
| "discarded", msg.len, VICI_MESSAGE_SIZE_MAX); |
| chunk_clear(&msg); |
| } |
| } |
| |
| CALLBACK(flush_messages, void, |
| entry_t *entry, va_list args) |
| { |
| private_vici_socket_t *this; |
| char errmsg[256] = ""; |
| bool ret; |
| |
| VA_ARGS_VGET(args, this); |
| |
| /* no need for any locking as no other threads are running, the connections |
| * all get disconnected afterwards, so error handling is simple too */ |
| ret = do_write(this, entry, entry->stream, errmsg, sizeof(errmsg), TRUE); |
| |
| if (!ret && errmsg[0]) |
| { |
| DBG1(DBG_CFG, errmsg); |
| } |
| } |
| |
| METHOD(vici_socket_t, destroy, void, |
| private_vici_socket_t *this) |
| { |
| DESTROY_IF(this->service); |
| this->connections->invoke_function(this->connections, flush_messages, this); |
| this->connections->destroy_function(this->connections, destroy_entry); |
| this->mutex->destroy(this->mutex); |
| free(this); |
| } |
| |
| /* |
| * see header file |
| */ |
| vici_socket_t *vici_socket_create(char *uri, vici_inbound_cb_t inbound, |
| vici_connect_cb_t connect, |
| vici_disconnect_cb_t disconnect, void *user) |
| { |
| private_vici_socket_t *this; |
| |
| INIT(this, |
| .public = { |
| .send = _send_, |
| .destroy = _destroy, |
| }, |
| .mutex = mutex_create(MUTEX_TYPE_DEFAULT), |
| .connections = linked_list_create(), |
| .inbound = inbound, |
| .connect = connect, |
| .disconnect = disconnect, |
| .user = user, |
| ); |
| |
| this->service = lib->streams->create_service(lib->streams, uri, 3); |
| if (!this->service) |
| { |
| DBG1(DBG_CFG, "creating vici socket failed"); |
| destroy(this); |
| return NULL; |
| } |
| this->service->on_accept(this->service, on_accept, this, |
| JOB_PRIO_CRITICAL, 0); |
| |
| return &this->public; |
| } |