From 7753a0ec750f17e1f014cda0dd2564ae0533cdd4 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Wed, 22 Nov 2017 21:37:07 -0500 Subject: [PATCH] Wire up IPC server --- include/sway/ipc-json.h | 7 + include/sway/ipc-server.h | 12 ++ sway/CMakeLists.txt | 2 + sway/ipc-json.c | 18 ++ sway/ipc-server.c | 411 ++++++++++++++++++++++++++++++++++++++ sway/main.c | 5 +- 6 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 include/sway/ipc-json.h create mode 100644 include/sway/ipc-server.h create mode 100644 sway/ipc-json.c create mode 100644 sway/ipc-server.c diff --git a/include/sway/ipc-json.h b/include/sway/ipc-json.h new file mode 100644 index 00000000..5bfddcba --- /dev/null +++ b/include/sway/ipc-json.h @@ -0,0 +1,7 @@ +#ifndef _SWAY_IPC_JSON_H +#define _SWAY_IPC_JSON_H +#include + +json_object *ipc_json_get_version(); + +#endif diff --git a/include/sway/ipc-server.h b/include/sway/ipc-server.h new file mode 100644 index 00000000..b85ff535 --- /dev/null +++ b/include/sway/ipc-server.h @@ -0,0 +1,12 @@ +#ifndef _SWAY_IPC_SERVER_H +#define _SWAY_IPC_SERVER_H +#include +#include "ipc.h" + +struct sway_server; + +void ipc_init(struct sway_server *server); +void ipc_terminate(void); +struct sockaddr_un *ipc_user_sockaddr(void); + +#endif diff --git a/sway/CMakeLists.txt b/sway/CMakeLists.txt index b85d619f..274fcc4a 100644 --- a/sway/CMakeLists.txt +++ b/sway/CMakeLists.txt @@ -24,6 +24,8 @@ add_executable(sway base64.c main.c server.c + ipc-json.c + ipc-server.c ) add_definitions( diff --git a/sway/ipc-json.c b/sway/ipc-json.c new file mode 100644 index 00000000..6fcb91fa --- /dev/null +++ b/sway/ipc-json.c @@ -0,0 +1,18 @@ +#include +#include +#include "sway/ipc-json.h" + +json_object *ipc_json_get_version() { + int major = 0, minor = 0, patch = 0; + json_object *version = json_object_new_object(); + + sscanf(SWAY_VERSION, "%u.%u.%u", &major, &minor, &patch); + + json_object_object_add(version, "human_readable", json_object_new_string(SWAY_VERSION)); + json_object_object_add(version, "variant", json_object_new_string("sway")); + json_object_object_add(version, "major", json_object_new_int(major)); + json_object_object_add(version, "minor", json_object_new_int(minor)); + json_object_object_add(version, "patch", json_object_new_int(patch)); + + return version; +} diff --git a/sway/ipc-server.c b/sway/ipc-server.c new file mode 100644 index 00000000..f3a6469b --- /dev/null +++ b/sway/ipc-server.c @@ -0,0 +1,411 @@ +// See https://i3wm.org/docs/ipc.html for protocol information +#ifndef __FreeBSD__ +// Any value will hide SOCK_CLOEXEC on FreeBSD (__BSD_VISIBLE=0) +#define _XOPEN_SOURCE 700 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/ipc-json.h" +#include "sway/ipc-server.h" +#include "sway/server.h" +#include "list.h" +#include "log.h" + +static int ipc_socket = -1; +static struct wl_event_source *ipc_event_source = NULL; +static struct sockaddr_un *ipc_sockaddr = NULL; +static list_t *ipc_client_list = NULL; + +static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'}; + +struct ipc_client { + struct wl_event_source *event_source; + struct wl_event_source *writable_event_source; + struct sway_server *server; + int fd; + uint32_t payload_length; + uint32_t security_policy; + enum ipc_command_type current_command; + enum ipc_command_type subscribed_events; + size_t write_buffer_len; + size_t write_buffer_size; + char *write_buffer; +}; + +struct sockaddr_un *ipc_user_sockaddr(void); +int ipc_handle_connection(int fd, uint32_t mask, void *data); +int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data); +int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data); +void ipc_client_disconnect(struct ipc_client *client); +void ipc_client_handle_command(struct ipc_client *client); +bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length); + +void ipc_init(struct sway_server *server) { + ipc_socket = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (ipc_socket == -1) { + sway_abort("Unable to create IPC socket"); + } + + ipc_sockaddr = ipc_user_sockaddr(); + + // We want to use socket name set by user, not existing socket from another sway instance. + if (getenv("SWAYSOCK") != NULL && access(getenv("SWAYSOCK"), F_OK) == -1) { + strncpy(ipc_sockaddr->sun_path, getenv("SWAYSOCK"), sizeof(ipc_sockaddr->sun_path)); + ipc_sockaddr->sun_path[sizeof(ipc_sockaddr->sun_path) - 1] = 0; + } + + unlink(ipc_sockaddr->sun_path); + if (bind(ipc_socket, (struct sockaddr *)ipc_sockaddr, sizeof(*ipc_sockaddr)) == -1) { + sway_abort("Unable to bind IPC socket"); + } + + if (listen(ipc_socket, 3) == -1) { + sway_abort("Unable to listen on IPC socket"); + } + + // Set i3 IPC socket path so that i3-msg works out of the box + setenv("I3SOCK", ipc_sockaddr->sun_path, 1); + setenv("SWAYSOCK", ipc_sockaddr->sun_path, 1); + + ipc_client_list = create_list(); + + ipc_event_source = wl_event_loop_add_fd(server->wl_event_loop, ipc_socket, + WL_EVENT_READABLE, ipc_handle_connection, server); +} + +void ipc_terminate(void) { + if (ipc_event_source) { + wl_event_source_remove(ipc_event_source); + } + close(ipc_socket); + unlink(ipc_sockaddr->sun_path); + + list_free(ipc_client_list); + + if (ipc_sockaddr) { + free(ipc_sockaddr); + } +} + +struct sockaddr_un *ipc_user_sockaddr(void) { + struct sockaddr_un *ipc_sockaddr = malloc(sizeof(struct sockaddr_un)); + if (ipc_sockaddr == NULL) { + sway_abort("Can't allocate ipc_sockaddr"); + } + + ipc_sockaddr->sun_family = AF_UNIX; + int path_size = sizeof(ipc_sockaddr->sun_path); + + // Env var typically set by logind, e.g. "/run/user/" + const char *dir = getenv("XDG_RUNTIME_DIR"); + if (!dir) { + dir = "/tmp"; + } + if (path_size <= snprintf(ipc_sockaddr->sun_path, path_size, + "%s/sway-ipc.%i.%i.sock", dir, getuid(), getpid())) { + sway_abort("Socket path won't fit into ipc_sockaddr->sun_path"); + } + + return ipc_sockaddr; +} + +int ipc_handle_connection(int fd, uint32_t mask, void *data) { + (void) fd; + struct sway_server *server = data; + sway_log(L_DEBUG, "Event on IPC listening socket"); + assert(mask == WL_EVENT_READABLE); + + int client_fd = accept(ipc_socket, NULL, NULL); + if (client_fd == -1) { + sway_log_errno(L_ERROR, "Unable to accept IPC client connection"); + return 0; + } + + int flags; + if ((flags = fcntl(client_fd, F_GETFD)) == -1 + || fcntl(client_fd, F_SETFD, flags|FD_CLOEXEC) == -1) { + sway_log_errno(L_ERROR, "Unable to set CLOEXEC on IPC client socket"); + close(client_fd); + return 0; + } + if ((flags = fcntl(client_fd, F_GETFL)) == -1 + || fcntl(client_fd, F_SETFL, flags|O_NONBLOCK) == -1) { + sway_log_errno(L_ERROR, "Unable to set NONBLOCK on IPC client socket"); + close(client_fd); + return 0; + } + + struct ipc_client *client = malloc(sizeof(struct ipc_client)); + if (!client) { + sway_log(L_ERROR, "Unable to allocate ipc client"); + close(client_fd); + return 0; + } + client->server = server; + client->payload_length = 0; + client->fd = client_fd; + client->subscribed_events = 0; + client->event_source = wl_event_loop_add_fd(server->wl_event_loop, + client_fd, WL_EVENT_READABLE, ipc_client_handle_readable, client); + client->writable_event_source = NULL; + + client->write_buffer_size = 128; + client->write_buffer_len = 0; + client->write_buffer = malloc(client->write_buffer_size); + if (!client->write_buffer) { + sway_log(L_ERROR, "Unable to allocate ipc client write buffer"); + close(client_fd); + return 0; + } + + sway_log(L_DEBUG, "New client: fd %d", client_fd); + list_add(ipc_client_list, client); + return 0; +} + +static const int ipc_header_size = sizeof(ipc_magic) + 8; + +int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data) { + struct ipc_client *client = data; + + if (mask & WL_EVENT_ERROR) { + sway_log(L_ERROR, "IPC Client socket error, removing client"); + ipc_client_disconnect(client); + return 0; + } + + if (mask & WL_EVENT_HANGUP) { + sway_log(L_DEBUG, "Client %d hung up", client->fd); + ipc_client_disconnect(client); + return 0; + } + + sway_log(L_DEBUG, "Client %d readable", client->fd); + + int read_available; + if (ioctl(client_fd, FIONREAD, &read_available) == -1) { + sway_log_errno(L_INFO, "Unable to read IPC socket buffer size"); + ipc_client_disconnect(client); + return 0; + } + + // Wait for the rest of the command payload in case the header has already been read + if (client->payload_length > 0) { + if ((uint32_t)read_available >= client->payload_length) { + ipc_client_handle_command(client); + } + return 0; + } + + if (read_available < ipc_header_size) { + return 0; + } + + uint8_t buf[ipc_header_size]; + uint32_t *buf32 = (uint32_t*)(buf + sizeof(ipc_magic)); + // Should be fully available, because read_available >= ipc_header_size + ssize_t received = recv(client_fd, buf, ipc_header_size, 0); + if (received == -1) { + sway_log_errno(L_INFO, "Unable to receive header from IPC client"); + ipc_client_disconnect(client); + return 0; + } + + if (memcmp(buf, ipc_magic, sizeof(ipc_magic)) != 0) { + sway_log(L_DEBUG, "IPC header check failed"); + ipc_client_disconnect(client); + return 0; + } + + client->payload_length = buf32[0]; + client->current_command = (enum ipc_command_type)buf32[1]; + + if (read_available - received >= (long)client->payload_length) { + ipc_client_handle_command(client); + } + + return 0; +} + +int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data) { + struct ipc_client *client = data; + + if (mask & WL_EVENT_ERROR) { + sway_log(L_ERROR, "IPC Client socket error, removing client"); + ipc_client_disconnect(client); + return 0; + } + + if (mask & WL_EVENT_HANGUP) { + sway_log(L_DEBUG, "Client %d hung up", client->fd); + ipc_client_disconnect(client); + return 0; + } + + if (client->write_buffer_len <= 0) { + return 0; + } + + sway_log(L_DEBUG, "Client %d writable", client->fd); + + ssize_t written = write(client->fd, client->write_buffer, client->write_buffer_len); + + if (written == -1 && errno == EAGAIN) { + return 0; + } else if (written == -1) { + sway_log_errno(L_INFO, "Unable to send data from queue to IPC client"); + ipc_client_disconnect(client); + return 0; + } + + memmove(client->write_buffer, client->write_buffer + written, client->write_buffer_len - written); + client->write_buffer_len -= written; + + if (client->write_buffer_len == 0 && client->writable_event_source) { + wl_event_source_remove(client->writable_event_source); + client->writable_event_source = NULL; + } + + return 0; +} + +void ipc_client_disconnect(struct ipc_client *client) { + if (!sway_assert(client != NULL, "client != NULL")) { + return; + } + + if (client->fd != -1) { + shutdown(client->fd, SHUT_RDWR); + } + + sway_log(L_INFO, "IPC Client %d disconnected", client->fd); + wl_event_source_remove(client->event_source); + if (client->writable_event_source) { + wl_event_source_remove(client->writable_event_source); + } + int i = 0; + while (i < ipc_client_list->length && ipc_client_list->items[i] != client) { + i++; + } + list_del(ipc_client_list, i); + free(client->write_buffer); + close(client->fd); + free(client); +} + +void ipc_client_handle_command(struct ipc_client *client) { + if (!sway_assert(client != NULL, "client != NULL")) { + return; + } + + char *buf = malloc(client->payload_length + 1); + if (!buf) { + sway_log_errno(L_INFO, "Unable to allocate IPC payload"); + ipc_client_disconnect(client); + return; + } + if (client->payload_length > 0) { + // Payload should be fully available + ssize_t received = recv(client->fd, buf, client->payload_length, 0); + if (received == -1) + { + sway_log_errno(L_INFO, "Unable to receive payload from IPC client"); + ipc_client_disconnect(client); + free(buf); + return; + } + } + buf[client->payload_length] = '\0'; + + const char *error_denied = "{ \"success\": false, \"error\": \"Permission denied\" }"; + + switch (client->current_command) { + case IPC_COMMAND: + { + struct cmd_results *results = handle_command(buf); + const char *json = cmd_results_to_json(results); + char reply[256]; + int length = snprintf(reply, sizeof(reply), "%s", json); + ipc_send_reply(client, reply, (uint32_t) length); + free_cmd_results(results); + goto exit_cleanup; + } + + case IPC_GET_VERSION: + { + json_object *version = ipc_json_get_version(); + const char *json_string = json_object_to_json_string(version); + ipc_send_reply(client, json_string, (uint32_t)strlen(json_string)); + json_object_put(version); // free + goto exit_cleanup; + } + + default: + sway_log(L_INFO, "Unknown IPC command type %i", client->current_command); + goto exit_cleanup; + } + + ipc_send_reply(client, error_denied, (uint32_t)strlen(error_denied)); + sway_log(L_DEBUG, "Denied IPC client access to %i", client->current_command); + +exit_cleanup: + client->payload_length = 0; + free(buf); + return; +} + +bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length) { + assert(payload); + + char data[ipc_header_size]; + uint32_t *data32 = (uint32_t*)(data + sizeof(ipc_magic)); + + memcpy(data, ipc_magic, sizeof(ipc_magic)); + data32[0] = payload_length; + data32[1] = client->current_command; + + while (client->write_buffer_len + ipc_header_size + payload_length >= + client->write_buffer_size) { + client->write_buffer_size *= 2; + } + + if (client->write_buffer_size > 4e6) { // 4 MB + sway_log(L_ERROR, "Client write buffer too big, disconnecting client"); + ipc_client_disconnect(client); + return false; + } + + char *new_buffer = realloc(client->write_buffer, client->write_buffer_size); + if (!new_buffer) { + sway_log(L_ERROR, "Unable to reallocate ipc client write buffer"); + ipc_client_disconnect(client); + return false; + } + client->write_buffer = new_buffer; + + memcpy(client->write_buffer + client->write_buffer_len, data, ipc_header_size); + client->write_buffer_len += ipc_header_size; + memcpy(client->write_buffer + client->write_buffer_len, payload, payload_length); + client->write_buffer_len += payload_length; + + if (!client->writable_event_source) { + client->writable_event_source = wl_event_loop_add_fd( + server.wl_event_loop, client->fd, WL_EVENT_WRITABLE, + ipc_client_handle_writable, client); + } + + sway_log(L_DEBUG, "Added IPC reply to client %d queue: %s", client->fd, payload); + return true; +} diff --git a/sway/main.c b/sway/main.c index 42262b05..5d5f9a57 100644 --- a/sway/main.c +++ b/sway/main.c @@ -17,6 +17,7 @@ #endif #include "sway/server.h" #include "sway/layout.h" +#include "sway/ipc-server.h" #include "ipc-client.h" #include "log.h" #include "readline.h" @@ -438,7 +439,7 @@ int main(int argc, char **argv) { } init_layout(); - //ipc_init(); + ipc_init(&server); //if (validate) { // bool valid = load_main_config(config_path, false); @@ -461,7 +462,7 @@ int main(int argc, char **argv) { server_fini(&server); - //ipc_terminate(); + ipc_terminate(); //if (config) { // free_config(config);