diff options
-rw-r--r-- | include/sway/ipc-json.h | 7 | ||||
-rw-r--r-- | include/sway/ipc-server.h | 12 | ||||
-rw-r--r-- | sway/CMakeLists.txt | 2 | ||||
-rw-r--r-- | sway/ipc-json.c | 18 | ||||
-rw-r--r-- | sway/ipc-server.c | 411 | ||||
-rw-r--r-- | sway/main.c | 5 |
6 files changed, 453 insertions, 2 deletions
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 @@ | |||
1 | #ifndef _SWAY_IPC_JSON_H | ||
2 | #define _SWAY_IPC_JSON_H | ||
3 | #include <json-c/json.h> | ||
4 | |||
5 | json_object *ipc_json_get_version(); | ||
6 | |||
7 | #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 @@ | |||
1 | #ifndef _SWAY_IPC_SERVER_H | ||
2 | #define _SWAY_IPC_SERVER_H | ||
3 | #include <sys/socket.h> | ||
4 | #include "ipc.h" | ||
5 | |||
6 | struct sway_server; | ||
7 | |||
8 | void ipc_init(struct sway_server *server); | ||
9 | void ipc_terminate(void); | ||
10 | struct sockaddr_un *ipc_user_sockaddr(void); | ||
11 | |||
12 | #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 | |||
24 | base64.c | 24 | base64.c |
25 | main.c | 25 | main.c |
26 | server.c | 26 | server.c |
27 | ipc-json.c | ||
28 | ipc-server.c | ||
27 | ) | 29 | ) |
28 | 30 | ||
29 | add_definitions( | 31 | 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 @@ | |||
1 | #include <json-c/json.h> | ||
2 | #include <stdio.h> | ||
3 | #include "sway/ipc-json.h" | ||
4 | |||
5 | json_object *ipc_json_get_version() { | ||
6 | int major = 0, minor = 0, patch = 0; | ||
7 | json_object *version = json_object_new_object(); | ||
8 | |||
9 | sscanf(SWAY_VERSION, "%u.%u.%u", &major, &minor, &patch); | ||
10 | |||
11 | json_object_object_add(version, "human_readable", json_object_new_string(SWAY_VERSION)); | ||
12 | json_object_object_add(version, "variant", json_object_new_string("sway")); | ||
13 | json_object_object_add(version, "major", json_object_new_int(major)); | ||
14 | json_object_object_add(version, "minor", json_object_new_int(minor)); | ||
15 | json_object_object_add(version, "patch", json_object_new_int(patch)); | ||
16 | |||
17 | return version; | ||
18 | } | ||
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 @@ | |||
1 | // See https://i3wm.org/docs/ipc.html for protocol information | ||
2 | #ifndef __FreeBSD__ | ||
3 | // Any value will hide SOCK_CLOEXEC on FreeBSD (__BSD_VISIBLE=0) | ||
4 | #define _XOPEN_SOURCE 700 | ||
5 | #endif | ||
6 | #include <assert.h> | ||
7 | #include <errno.h> | ||
8 | #include <fcntl.h> | ||
9 | #include <json-c/json.h> | ||
10 | #include <stdbool.h> | ||
11 | #include <stdint.h> | ||
12 | #include <stdlib.h> | ||
13 | #include <string.h> | ||
14 | #include <sys/socket.h> | ||
15 | #include <sys/ioctl.h> | ||
16 | #include <sys/un.h> | ||
17 | #include <unistd.h> | ||
18 | #include <wayland-server.h> | ||
19 | #include "sway/commands.h" | ||
20 | #include "sway/ipc-json.h" | ||
21 | #include "sway/ipc-server.h" | ||
22 | #include "sway/server.h" | ||
23 | #include "list.h" | ||
24 | #include "log.h" | ||
25 | |||
26 | static int ipc_socket = -1; | ||
27 | static struct wl_event_source *ipc_event_source = NULL; | ||
28 | static struct sockaddr_un *ipc_sockaddr = NULL; | ||
29 | static list_t *ipc_client_list = NULL; | ||
30 | |||
31 | static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'}; | ||
32 | |||
33 | struct ipc_client { | ||
34 | struct wl_event_source *event_source; | ||
35 | struct wl_event_source *writable_event_source; | ||
36 | struct sway_server *server; | ||
37 | int fd; | ||
38 | uint32_t payload_length; | ||
39 | uint32_t security_policy; | ||
40 | enum ipc_command_type current_command; | ||
41 | enum ipc_command_type subscribed_events; | ||
42 | size_t write_buffer_len; | ||
43 | size_t write_buffer_size; | ||
44 | char *write_buffer; | ||
45 | }; | ||
46 | |||
47 | struct sockaddr_un *ipc_user_sockaddr(void); | ||
48 | int ipc_handle_connection(int fd, uint32_t mask, void *data); | ||
49 | int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data); | ||
50 | int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data); | ||
51 | void ipc_client_disconnect(struct ipc_client *client); | ||
52 | void ipc_client_handle_command(struct ipc_client *client); | ||
53 | bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length); | ||
54 | |||
55 | void ipc_init(struct sway_server *server) { | ||
56 | ipc_socket = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); | ||
57 | if (ipc_socket == -1) { | ||
58 | sway_abort("Unable to create IPC socket"); | ||
59 | } | ||
60 | |||
61 | ipc_sockaddr = ipc_user_sockaddr(); | ||
62 | |||
63 | // We want to use socket name set by user, not existing socket from another sway instance. | ||
64 | if (getenv("SWAYSOCK") != NULL && access(getenv("SWAYSOCK"), F_OK) == -1) { | ||
65 | strncpy(ipc_sockaddr->sun_path, getenv("SWAYSOCK"), sizeof(ipc_sockaddr->sun_path)); | ||
66 | ipc_sockaddr->sun_path[sizeof(ipc_sockaddr->sun_path) - 1] = 0; | ||
67 | } | ||
68 | |||
69 | unlink(ipc_sockaddr->sun_path); | ||
70 | if (bind(ipc_socket, (struct sockaddr *)ipc_sockaddr, sizeof(*ipc_sockaddr)) == -1) { | ||
71 | sway_abort("Unable to bind IPC socket"); | ||
72 | } | ||
73 | |||
74 | if (listen(ipc_socket, 3) == -1) { | ||
75 | sway_abort("Unable to listen on IPC socket"); | ||
76 | } | ||
77 | |||
78 | // Set i3 IPC socket path so that i3-msg works out of the box | ||
79 | setenv("I3SOCK", ipc_sockaddr->sun_path, 1); | ||
80 | setenv("SWAYSOCK", ipc_sockaddr->sun_path, 1); | ||
81 | |||
82 | ipc_client_list = create_list(); | ||
83 | |||
84 | ipc_event_source = wl_event_loop_add_fd(server->wl_event_loop, ipc_socket, | ||
85 | WL_EVENT_READABLE, ipc_handle_connection, server); | ||
86 | } | ||
87 | |||
88 | void ipc_terminate(void) { | ||
89 | if (ipc_event_source) { | ||
90 | wl_event_source_remove(ipc_event_source); | ||
91 | } | ||
92 | close(ipc_socket); | ||
93 | unlink(ipc_sockaddr->sun_path); | ||
94 | |||
95 | list_free(ipc_client_list); | ||
96 | |||
97 | if (ipc_sockaddr) { | ||
98 | free(ipc_sockaddr); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | struct sockaddr_un *ipc_user_sockaddr(void) { | ||
103 | struct sockaddr_un *ipc_sockaddr = malloc(sizeof(struct sockaddr_un)); | ||
104 | if (ipc_sockaddr == NULL) { | ||
105 | sway_abort("Can't allocate ipc_sockaddr"); | ||
106 | } | ||
107 | |||
108 | ipc_sockaddr->sun_family = AF_UNIX; | ||
109 | int path_size = sizeof(ipc_sockaddr->sun_path); | ||
110 | |||
111 | // Env var typically set by logind, e.g. "/run/user/<user-id>" | ||
112 | const char *dir = getenv("XDG_RUNTIME_DIR"); | ||
113 | if (!dir) { | ||
114 | dir = "/tmp"; | ||
115 | } | ||
116 | if (path_size <= snprintf(ipc_sockaddr->sun_path, path_size, | ||
117 | "%s/sway-ipc.%i.%i.sock", dir, getuid(), getpid())) { | ||
118 | sway_abort("Socket path won't fit into ipc_sockaddr->sun_path"); | ||
119 | } | ||
120 | |||
121 | return ipc_sockaddr; | ||
122 | } | ||
123 | |||
124 | int ipc_handle_connection(int fd, uint32_t mask, void *data) { | ||
125 | (void) fd; | ||
126 | struct sway_server *server = data; | ||
127 | sway_log(L_DEBUG, "Event on IPC listening socket"); | ||
128 | assert(mask == WL_EVENT_READABLE); | ||
129 | |||
130 | int client_fd = accept(ipc_socket, NULL, NULL); | ||
131 | if (client_fd == -1) { | ||
132 | sway_log_errno(L_ERROR, "Unable to accept IPC client connection"); | ||
133 | return 0; | ||
134 | } | ||
135 | |||
136 | int flags; | ||
137 | if ((flags = fcntl(client_fd, F_GETFD)) == -1 | ||
138 | || fcntl(client_fd, F_SETFD, flags|FD_CLOEXEC) == -1) { | ||
139 | sway_log_errno(L_ERROR, "Unable to set CLOEXEC on IPC client socket"); | ||
140 | close(client_fd); | ||
141 | return 0; | ||
142 | } | ||
143 | if ((flags = fcntl(client_fd, F_GETFL)) == -1 | ||
144 | || fcntl(client_fd, F_SETFL, flags|O_NONBLOCK) == -1) { | ||
145 | sway_log_errno(L_ERROR, "Unable to set NONBLOCK on IPC client socket"); | ||
146 | close(client_fd); | ||
147 | return 0; | ||
148 | } | ||
149 | |||
150 | struct ipc_client *client = malloc(sizeof(struct ipc_client)); | ||
151 | if (!client) { | ||
152 | sway_log(L_ERROR, "Unable to allocate ipc client"); | ||
153 | close(client_fd); | ||
154 | return 0; | ||
155 | } | ||
156 | client->server = server; | ||
157 | client->payload_length = 0; | ||
158 | client->fd = client_fd; | ||
159 | client->subscribed_events = 0; | ||
160 | client->event_source = wl_event_loop_add_fd(server->wl_event_loop, | ||
161 | client_fd, WL_EVENT_READABLE, ipc_client_handle_readable, client); | ||
162 | client->writable_event_source = NULL; | ||
163 | |||
164 | client->write_buffer_size = 128; | ||
165 | client->write_buffer_len = 0; | ||
166 | client->write_buffer = malloc(client->write_buffer_size); | ||
167 | if (!client->write_buffer) { | ||
168 | sway_log(L_ERROR, "Unable to allocate ipc client write buffer"); | ||
169 | close(client_fd); | ||
170 | return 0; | ||
171 | } | ||
172 | |||
173 | sway_log(L_DEBUG, "New client: fd %d", client_fd); | ||
174 | list_add(ipc_client_list, client); | ||
175 | return 0; | ||
176 | } | ||
177 | |||
178 | static const int ipc_header_size = sizeof(ipc_magic) + 8; | ||
179 | |||
180 | int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data) { | ||
181 | struct ipc_client *client = data; | ||
182 | |||
183 | if (mask & WL_EVENT_ERROR) { | ||
184 | sway_log(L_ERROR, "IPC Client socket error, removing client"); | ||
185 | ipc_client_disconnect(client); | ||
186 | return 0; | ||
187 | } | ||
188 | |||
189 | if (mask & WL_EVENT_HANGUP) { | ||
190 | sway_log(L_DEBUG, "Client %d hung up", client->fd); | ||
191 | ipc_client_disconnect(client); | ||
192 | return 0; | ||
193 | } | ||
194 | |||
195 | sway_log(L_DEBUG, "Client %d readable", client->fd); | ||
196 | |||
197 | int read_available; | ||
198 | if (ioctl(client_fd, FIONREAD, &read_available) == -1) { | ||
199 | sway_log_errno(L_INFO, "Unable to read IPC socket buffer size"); | ||
200 | ipc_client_disconnect(client); | ||
201 | return 0; | ||
202 | } | ||
203 | |||
204 | // Wait for the rest of the command payload in case the header has already been read | ||
205 | if (client->payload_length > 0) { | ||
206 | if ((uint32_t)read_available >= client->payload_length) { | ||
207 | ipc_client_handle_command(client); | ||
208 | } | ||
209 | return 0; | ||
210 | } | ||
211 | |||
212 | if (read_available < ipc_header_size) { | ||
213 | return 0; | ||
214 | } | ||
215 | |||
216 | uint8_t buf[ipc_header_size]; | ||
217 | uint32_t *buf32 = (uint32_t*)(buf + sizeof(ipc_magic)); | ||
218 | // Should be fully available, because read_available >= ipc_header_size | ||
219 | ssize_t received = recv(client_fd, buf, ipc_header_size, 0); | ||
220 | if (received == -1) { | ||
221 | sway_log_errno(L_INFO, "Unable to receive header from IPC client"); | ||
222 | ipc_client_disconnect(client); | ||
223 | return 0; | ||
224 | } | ||
225 | |||
226 | if (memcmp(buf, ipc_magic, sizeof(ipc_magic)) != 0) { | ||
227 | sway_log(L_DEBUG, "IPC header check failed"); | ||
228 | ipc_client_disconnect(client); | ||
229 | return 0; | ||
230 | } | ||
231 | |||
232 | client->payload_length = buf32[0]; | ||
233 | client->current_command = (enum ipc_command_type)buf32[1]; | ||
234 | |||
235 | if (read_available - received >= (long)client->payload_length) { | ||
236 | ipc_client_handle_command(client); | ||
237 | } | ||
238 | |||
239 | return 0; | ||
240 | } | ||
241 | |||
242 | int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data) { | ||
243 | struct ipc_client *client = data; | ||
244 | |||
245 | if (mask & WL_EVENT_ERROR) { | ||
246 | sway_log(L_ERROR, "IPC Client socket error, removing client"); | ||
247 | ipc_client_disconnect(client); | ||
248 | return 0; | ||
249 | } | ||
250 | |||
251 | if (mask & WL_EVENT_HANGUP) { | ||
252 | sway_log(L_DEBUG, "Client %d hung up", client->fd); | ||
253 | ipc_client_disconnect(client); | ||
254 | return 0; | ||
255 | } | ||
256 | |||
257 | if (client->write_buffer_len <= 0) { | ||
258 | return 0; | ||
259 | } | ||
260 | |||
261 | sway_log(L_DEBUG, "Client %d writable", client->fd); | ||
262 | |||
263 | ssize_t written = write(client->fd, client->write_buffer, client->write_buffer_len); | ||
264 | |||
265 | if (written == -1 && errno == EAGAIN) { | ||
266 | return 0; | ||
267 | } else if (written == -1) { | ||
268 | sway_log_errno(L_INFO, "Unable to send data from queue to IPC client"); | ||
269 | ipc_client_disconnect(client); | ||
270 | return 0; | ||
271 | } | ||
272 | |||
273 | memmove(client->write_buffer, client->write_buffer + written, client->write_buffer_len - written); | ||
274 | client->write_buffer_len -= written; | ||
275 | |||
276 | if (client->write_buffer_len == 0 && client->writable_event_source) { | ||
277 | wl_event_source_remove(client->writable_event_source); | ||
278 | client->writable_event_source = NULL; | ||
279 | } | ||
280 | |||
281 | return 0; | ||
282 | } | ||
283 | |||
284 | void ipc_client_disconnect(struct ipc_client *client) { | ||
285 | if (!sway_assert(client != NULL, "client != NULL")) { | ||
286 | return; | ||
287 | } | ||
288 | |||
289 | if (client->fd != -1) { | ||
290 | shutdown(client->fd, SHUT_RDWR); | ||
291 | } | ||
292 | |||
293 | sway_log(L_INFO, "IPC Client %d disconnected", client->fd); | ||
294 | wl_event_source_remove(client->event_source); | ||
295 | if (client->writable_event_source) { | ||
296 | wl_event_source_remove(client->writable_event_source); | ||
297 | } | ||
298 | int i = 0; | ||
299 | while (i < ipc_client_list->length && ipc_client_list->items[i] != client) { | ||
300 | i++; | ||
301 | } | ||
302 | list_del(ipc_client_list, i); | ||
303 | free(client->write_buffer); | ||
304 | close(client->fd); | ||
305 | free(client); | ||
306 | } | ||
307 | |||
308 | void ipc_client_handle_command(struct ipc_client *client) { | ||
309 | if (!sway_assert(client != NULL, "client != NULL")) { | ||
310 | return; | ||
311 | } | ||
312 | |||
313 | char *buf = malloc(client->payload_length + 1); | ||
314 | if (!buf) { | ||
315 | sway_log_errno(L_INFO, "Unable to allocate IPC payload"); | ||
316 | ipc_client_disconnect(client); | ||
317 | return; | ||
318 | } | ||
319 | if (client->payload_length > 0) { | ||
320 | // Payload should be fully available | ||
321 | ssize_t received = recv(client->fd, buf, client->payload_length, 0); | ||
322 | if (received == -1) | ||
323 | { | ||
324 | sway_log_errno(L_INFO, "Unable to receive payload from IPC client"); | ||
325 | ipc_client_disconnect(client); | ||
326 | free(buf); | ||
327 | return; | ||
328 | } | ||
329 | } | ||
330 | buf[client->payload_length] = '\0'; | ||
331 | |||
332 | const char *error_denied = "{ \"success\": false, \"error\": \"Permission denied\" }"; | ||
333 | |||
334 | switch (client->current_command) { | ||
335 | case IPC_COMMAND: | ||
336 | { | ||
337 | struct cmd_results *results = handle_command(buf); | ||
338 | const char *json = cmd_results_to_json(results); | ||
339 | char reply[256]; | ||
340 | int length = snprintf(reply, sizeof(reply), "%s", json); | ||
341 | ipc_send_reply(client, reply, (uint32_t) length); | ||
342 | free_cmd_results(results); | ||
343 | goto exit_cleanup; | ||
344 | } | ||
345 | |||
346 | case IPC_GET_VERSION: | ||
347 | { | ||
348 | json_object *version = ipc_json_get_version(); | ||
349 | const char *json_string = json_object_to_json_string(version); | ||
350 | ipc_send_reply(client, json_string, (uint32_t)strlen(json_string)); | ||
351 | json_object_put(version); // free | ||
352 | goto exit_cleanup; | ||
353 | } | ||
354 | |||
355 | default: | ||
356 | sway_log(L_INFO, "Unknown IPC command type %i", client->current_command); | ||
357 | goto exit_cleanup; | ||
358 | } | ||
359 | |||
360 | ipc_send_reply(client, error_denied, (uint32_t)strlen(error_denied)); | ||
361 | sway_log(L_DEBUG, "Denied IPC client access to %i", client->current_command); | ||
362 | |||
363 | exit_cleanup: | ||
364 | client->payload_length = 0; | ||
365 | free(buf); | ||
366 | return; | ||
367 | } | ||
368 | |||
369 | bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length) { | ||
370 | assert(payload); | ||
371 | |||
372 | char data[ipc_header_size]; | ||
373 | uint32_t *data32 = (uint32_t*)(data + sizeof(ipc_magic)); | ||
374 | |||
375 | memcpy(data, ipc_magic, sizeof(ipc_magic)); | ||
376 | data32[0] = payload_length; | ||
377 | data32[1] = client->current_command; | ||
378 | |||
379 | while (client->write_buffer_len + ipc_header_size + payload_length >= | ||
380 | client->write_buffer_size) { | ||
381 | client->write_buffer_size *= 2; | ||
382 | } | ||
383 | |||
384 | if (client->write_buffer_size > 4e6) { // 4 MB | ||
385 | sway_log(L_ERROR, "Client write buffer too big, disconnecting client"); | ||
386 | ipc_client_disconnect(client); | ||
387 | return false; | ||
388 | } | ||
389 | |||
390 | char *new_buffer = realloc(client->write_buffer, client->write_buffer_size); | ||
391 | if (!new_buffer) { | ||
392 | sway_log(L_ERROR, "Unable to reallocate ipc client write buffer"); | ||
393 | ipc_client_disconnect(client); | ||
394 | return false; | ||
395 | } | ||
396 | client->write_buffer = new_buffer; | ||
397 | |||
398 | memcpy(client->write_buffer + client->write_buffer_len, data, ipc_header_size); | ||
399 | client->write_buffer_len += ipc_header_size; | ||
400 | memcpy(client->write_buffer + client->write_buffer_len, payload, payload_length); | ||
401 | client->write_buffer_len += payload_length; | ||
402 | |||
403 | if (!client->writable_event_source) { | ||
404 | client->writable_event_source = wl_event_loop_add_fd( | ||
405 | server.wl_event_loop, client->fd, WL_EVENT_WRITABLE, | ||
406 | ipc_client_handle_writable, client); | ||
407 | } | ||
408 | |||
409 | sway_log(L_DEBUG, "Added IPC reply to client %d queue: %s", client->fd, payload); | ||
410 | return true; | ||
411 | } | ||
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 @@ | |||
17 | #endif | 17 | #endif |
18 | #include "sway/server.h" | 18 | #include "sway/server.h" |
19 | #include "sway/layout.h" | 19 | #include "sway/layout.h" |
20 | #include "sway/ipc-server.h" | ||
20 | #include "ipc-client.h" | 21 | #include "ipc-client.h" |
21 | #include "log.h" | 22 | #include "log.h" |
22 | #include "readline.h" | 23 | #include "readline.h" |
@@ -438,7 +439,7 @@ int main(int argc, char **argv) { | |||
438 | } | 439 | } |
439 | 440 | ||
440 | init_layout(); | 441 | init_layout(); |
441 | //ipc_init(); | 442 | ipc_init(&server); |
442 | 443 | ||
443 | //if (validate) { | 444 | //if (validate) { |
444 | // bool valid = load_main_config(config_path, false); | 445 | // bool valid = load_main_config(config_path, false); |
@@ -461,7 +462,7 @@ int main(int argc, char **argv) { | |||
461 | 462 | ||
462 | server_fini(&server); | 463 | server_fini(&server); |
463 | 464 | ||
464 | //ipc_terminate(); | 465 | ipc_terminate(); |
465 | 466 | ||
466 | //if (config) { | 467 | //if (config) { |
467 | // free_config(config); | 468 | // free_config(config); |