Dillo v3.2.0-170-gacf511bb
Loading...
Searching...
No Matches
control.c
Go to the documentation of this file.
1/*
2 * File: control.c
3 *
4 * Copyright (C) 2025-2026 Rodrigo Arias Mallo <rodarima@gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 */
11
18#include "control.h"
19
20#include "config.h"
21
22#if ENABLE_CONTROL_SOCKET
23
24#include <assert.h>
25#include <errno.h>
26#include <fcntl.h>
27#include <netinet/in.h>
28#include <stdlib.h>
29#include <sys/ioctl.h>
30#include <sys/socket.h>
31#include <sys/stat.h>
32#include <sys/un.h>
33#include <unistd.h>
34
35#include "iowatch.hh"
36#include "uicmd.hh"
37#include "capi.h"
38#include "dlib/dlib.h"
39#include "msg.h"
40#include "timeout.hh"
41
42enum control_req_stage {
43 REQ_READ_COMMAND,
44 REQ_READ_BODY,
45 REQ_PREPARE_REPLY,
46 REQ_SWITCH_WRITE,
47 REQ_WRITE_REPLY,
48 REQ_CLOSE,
49};
50
51struct control_req {
52 int fd;
53 enum control_req_stage stage;
54 Dstr *cmd;
55 Dstr *body;
56 Dstr *reply;
57 size_t reply_offset;
58};
59
60/* One control fd per process */
61static int control_fd = -1;
62static char *control_path = NULL;
63
64/* The BrowserWindow that we are currently waiting to finish */
65static BrowserWindow *bw_waiting = NULL;
66static struct control_req *bw_waiting_req = NULL;
67
68static void handle_wait_timeout(void *data);
69
70/* Read the given non-blocking fd, read available data into output.
71 * Returns:
72 * 0: when the end of file has been reached.
73 * +1: if there is more data to be read.
74 * <0: other unexpected error.
75 */
76static int
77Control_read_fd(int fd, Dstr *output)
78{
79 ssize_t r;
80 do {
81 char buf[BUFSIZ];
82 r = read(fd, buf, BUFSIZ);
83 _MSG("read buf = %zd\n", r);
84 if (r < 0) {
85 if (errno == EINTR) {
86 continue;
87 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
88 /* Reached end of available data, try later */
89 return +1;
90 } else {
91 MSG_ERR("Control_read_fd: read failed: %s\n", dStrerror(errno));
92 return -1;
93 }
94 } else if (r > 0) {
95 dStr_append_l(output, buf, r);
96 }
97 } while (r);
98
99 return 0;
100}
101
102static int
103Control_write_fd(int fd, Dstr *input, size_t *ptr_offset)
104{
105 size_t offset = *ptr_offset;
106 ssize_t w;
107
108 while ((size_t) input->len > offset) {
109 const char *src = input->str + offset;
110 size_t left = input->len - offset;
111 w = write(fd, src, left);
112 if (w < 0) {
113 if (errno == EINTR) {
114 continue;
115 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
116 /* Cannot write more data, return error */
117 *ptr_offset = offset;
118 return +1;
119 } else {
120 MSG_ERR("Control_write_fd: write failed: %s\n", dStrerror(errno));
121 return -1;
122 }
123 } else if (w > 0) {
124 offset += w;
125 }
126 }
127
128 /* Save offset just in case */
129 *ptr_offset = offset;
130
131 return 0;
132}
133
134/* Create new request structure */
135static struct control_req *Control_req_new(int fd)
136{
137 struct control_req *req = dMalloc0(sizeof(struct control_req));
138 req->fd = fd;
139 req->cmd = dStr_sized_new(128);
140 req->body = dStr_sized_new(1);
141 req->reply = dStr_sized_new(64);
142 req->stage = REQ_READ_COMMAND;
143
144 return req;
145}
146
147static void Control_req_free(struct control_req *req)
148{
149 dStr_free(req->cmd, 1);
150 dStr_free(req->body, 1);
151 dStr_free(req->reply, 1);
152 dFree(req);
153}
154
155static void Control_req_read_cmd(struct control_req *req)
156{
157 ssize_t r;
158 int err = 0;
159 do {
160 /* Read characters one by one */
161 char buf[1];
162 r = read(req->fd, buf, 1);
163 if (r < 0) {
164 if (errno == EINTR) {
165 continue;
166 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
167 /* Reached end of available data for now, try later */
168 return;
169 } else {
170 MSG_ERR("Control_req_read_cmd: read failed: %s\n", dStrerror(errno));
171 err = 1;
172 break;
173 }
174 } else if (r == 1) {
175 dStr_append_l(req->cmd, buf, 1);
176 }
177 if (buf[0] == '\n')
178 break;
179 } while (r);
180
181 if (err || req->cmd->len == 0) {
182 /* Problem reading command or empty (may be caused by a reset
183 * connection). Go straight to close the request. */
184 req->stage = REQ_CLOSE;
185 } else {
186 /* Got command, strip new line and line feed */
187 char *cmd = req->cmd->str;
188 for (char *p = cmd; *p != '\0'; p++) {
189 if (*p == '\n' || *p == '\r') {
190 *p = '\0';
191 break;
192 }
193 }
194
195 /* Only determine if we need to read the body or not */
196 if (!strcmp(cmd, "load") || !strcmp(cmd, "rawload"))
197 req->stage = REQ_READ_BODY;
198 else
199 req->stage = REQ_PREPARE_REPLY;
200 }
201}
202
203static void Control_req_prepare_reply(struct control_req *req)
204{
205 int done = 1;
206 const char *cmd = req->cmd->str;
207 Dstr *r = req->reply;
209
210 if (strcmp(cmd, "help") == 0) {
211 dStr_sprintfa(r, "0\n");
212 dStr_sprintfa(r, "Commands return 0 on success or non-zero on error.\n");
213 dStr_sprintfa(r, "Available commands:\n");
214 dStr_sprintfa(r, " ping Check if dillo replies correctly:\n");
215 dStr_sprintfa(r, " pid Print PID of selected dillo process\n");
216 dStr_sprintfa(r, " reload Reload the current tab\n");
217 dStr_sprintfa(r, " ready Exits with 0 if finished loading, 1 otherwise\n");
218 dStr_sprintfa(r, " open URL Open the given URL in the current tab\n");
219 dStr_sprintfa(r, " url Print the url in the current tab\n");
220 dStr_sprintfa(r, " title Print the title of page in the current tab\n");
221 dStr_sprintfa(r, " status [MSG] Set the status bar to MSG\n");
222 dStr_sprintfa(r, " dump Print the content of the current tab\n");
223 dStr_sprintfa(r, " hdump Print the HTTP headers of the current tab\n");
224 dStr_sprintfa(r, " load Replace the content in the current tab by stdin\n");
225 dStr_sprintfa(r, " rawload Replace the HTTP headers and content of the current\n");
226 dStr_sprintfa(r, " tab by stdin\n");
227 dStr_sprintfa(r, " quit Close dillo\n");
228 dStr_sprintfa(r, " wait [T] Wait until the current tab has finished loading\n");
229 dStr_sprintfa(r, " at most T seconds (default 60.0). Wait forever with\n");
230 dStr_sprintfa(r, " T set to 0.\n");
231 } else if (strcmp(cmd, "ping") == 0) {
232 dStr_sprintfa(r, "0\npong\n");
233 } else if (strcmp(cmd, "pid") == 0) {
234 dStr_sprintfa(r, "0\n%d\n", getpid());
235 } else if (strcmp(cmd, "reload") == 0) {
237 dStr_sprintfa(r, "0\n");
238 } else if (strcmp(cmd, "ready") == 0) {
239 dStr_sprintfa(r, "%d\n", a_UIcmd_has_finished(bw) ? 0 : 1);
240 } else if (strncmp(cmd, "open ", 5) == 0) {
241 a_UIcmd_open_urlstr(bw, cmd+5);
242 dStr_sprintfa(r, "0\n");
243 } else if (strcmp(cmd, "url") == 0) {
244 dStr_sprintfa(r, "0\n%s\n", a_UIcmd_get_location_text(bw));
245 } else if (strcmp(cmd, "title") == 0) {
246 const char *title = a_UIcmd_get_page_title(bw);
247 if (title)
248 dStr_sprintfa(r, "0\n%s\n", title);
249 else
250 dStr_sprintfa(r, "1\n");
251 } else if (!strcmp(cmd, "status") || !strncmp(cmd, "status ", 7)) {
252 if (cmd[6] == ' ')
253 a_UIcmd_set_msg(bw, "%s", cmd + 7);
254 else
255 a_UIcmd_set_msg(bw, "");
256 dStr_sprintfa(r, "0\n");
257 } else if (strcmp(cmd, "dump") == 0) {
259 char *buf;
260 int size;
261 dStr_sprintfa(r, "0\n");
262 if (a_Capi_get_buf(url, &buf, &size))
263 dStr_append_l(r, buf, size);
264 a_Url_free(url);
265 } else if (strcmp(cmd, "hdump") == 0) {
267 Dstr *header = a_Cache_get_header(url);
268 if (header == NULL) {
269 dStr_sprintfa(r, "1\nCurrent url not cached");
270 } else {
271 dStr_sprintfa(r, "0\n");
272 dStr_append_l(r, header->str, header->len);
273 }
274 a_Url_free(url);
275 } else if (strcmp(cmd, "load") == 0) {
277 if (url) {
278 a_Cache_entry_inject(url, req->body->str, req->body->len,
280 a_UIcmd_repush(bw);
281 dStr_sprintfa(r, "0\n");
282 a_Url_free(url);
283 } else {
284 dStr_sprintfa(r, "1\ncannot get current url");
285 }
286 } else if (strcmp(cmd, "rawload") == 0) {
288 if (url) {
289 a_Cache_entry_inject(url, req->body->str, req->body->len, 0);
290 a_UIcmd_repush(bw);
291 dStr_sprintfa(r, "0\n");
292 a_Url_free(url);
293 } else {
294 dStr_sprintfa(r, "1\ncannot get current url");
295 }
296 } else if (strcmp(cmd, "quit") == 0) {
297 a_UIcmd_close_all_bw((void *)1);
298 dStr_sprintfa(r, "0\n");
299 } else if (strncmp(cmd, "cmd ", 4) == 0) {
300 const char *cmdname = cmd + 4;
301 int ret = a_UIcmd_by_name(bw, cmdname);
302 dStr_sprintfa(r, "%d\n", ret == 0 ? 0 : 1);
303 } else if (strcmp(cmd, "wait") == 0 || strncmp(cmd, "wait ", 5) == 0) {
304 float timeout = 60.0f; /* 1 minute by default */
305 /* Contains timeout argument? */
306 if (cmd[4] == ' ')
307 timeout = atof(&cmd[5]);
308 if (a_UIcmd_has_finished(bw)) {
309 dStr_sprintfa(r, "0\n");
310 } else {
311 if (bw_waiting) {
312 dStr_sprintfa(r, "1\nalready waiting\n");
313 } else {
314 bw_waiting = bw;
315 bw_waiting_req = req;
316 done = 0;
317 /* If timeout is 0 or negative, wait forever */
318 if (timeout > 0.0f)
319 a_Timeout_add(timeout, handle_wait_timeout, NULL);
320 }
321 }
322 } else {
323 dStr_sprintfa(r, "1\nunknown command: %s\n", cmd);
324 }
325
326 if (done)
327 req->stage = REQ_SWITCH_WRITE;
328}
329
330/* Process incoming or outgoing data from a control request */
331static void Control_req_read_cb(int fd, void *data)
332{
333 struct control_req *req = data;
334
335 /* Note: These are not if-elses on purpose. */
336
337 if (req->stage == REQ_READ_COMMAND)
338 Control_req_read_cmd(req);
339
340 if (req->stage == REQ_READ_BODY) {
341 /* Read until the end of the data or error */
342 int ret = Control_read_fd(fd, req->body);
343 if (ret == 0)
344 req->stage = REQ_PREPARE_REPLY;
345 else if (ret < 0)
346 req->stage = REQ_CLOSE;
347 }
348
349 if (req->stage == REQ_PREPARE_REPLY)
350 Control_req_prepare_reply(req);
351
352 if (req->stage == REQ_SWITCH_WRITE) {
353 /* Switch callback to write */
355 a_IOwatch_add_fd(fd, DIO_WRITE, Control_req_read_cb, req);
356 req->stage = REQ_WRITE_REPLY;
357 }
358
359 if (req->stage == REQ_WRITE_REPLY) {
360 /* Keep writing until end or error */
361 if (Control_write_fd(fd, req->reply, &req->reply_offset) <= 0)
362 req->stage = REQ_CLOSE;
363 }
364
365 if (req->stage == REQ_CLOSE) {
366 /* Remove any callback */
368 dClose(fd);
369 Control_req_free(req);
370 }
371}
372
373/* Process incoming connection to control socket */
374static void Control_listen_cb(int fd, void *data)
375{
376 _MSG("Control_listen_cb called\n");
377
378 int new_fd = accept(control_fd, NULL, NULL);
379 if (new_fd < 0) {
380 if (errno == EAGAIN || errno == EWOULDBLOCK) {
381 /* Nothing? */
382 return;
383 } else {
384 MSG_ERR("accept failed: %s\n", strerror(errno));
385 exit(1);
386 }
387 }
388
389 /* From accept(2) man page:
390 *
391 * On Linux, the new socket returned by accept() does not inherit
392 * file status flags such as O_NONBLOCK and O_ASYNC from the
393 * listening socket. This behavior differs from the canonical BSD
394 * sockets implementation. Portable programs should not rely on
395 * inheritance or noninheritance of file status flags and always
396 * explicitly set all required flags on the socket returned from
397 * accept().
398 *
399 * So, enable O_NONBLOCK flag explicitly.
400 */
401 if (fcntl(new_fd, F_SETFL, O_NONBLOCK) == -1) {
402 MSG_ERR("control socket fcntl failed: %s\n", strerror(errno));
403 exit(1);
404 }
405
406 struct control_req *req = Control_req_new(new_fd);
407 a_IOwatch_add_fd(new_fd, DIO_READ, Control_req_read_cb, req);
408}
409
410/* Timeout has passed, return error */
411static void handle_wait_timeout(void *data)
412{
413 assert(bw_waiting);
414 assert(bw_waiting_req);
415
416 struct control_req *req = bw_waiting_req;
417 dStr_sprintfa(req->reply, "1\nCurrent url not cached");
418 req->stage = REQ_SWITCH_WRITE;
419
420 bw_waiting = NULL;
421
422 /* Run another pass to do progress */
423 Control_req_read_cb(req->fd, req);
424}
425
426int a_Control_is_waiting(void)
427{
428 return bw_waiting != NULL;
429}
430
432{
433 if (bw_waiting == NULL || bw_waiting != bw)
434 return;
435
436 _MSG("a_Control_notify_finish matched\n");
437
438 assert(bw_waiting_req);
439
440 struct control_req *req = bw_waiting_req;
441 dStr_sprintfa(req->reply, "0\n");
442 req->stage = REQ_SWITCH_WRITE;
443
444 a_Timeout_actually_remove(handle_wait_timeout, NULL);
445 bw_waiting = NULL;
446 bw_waiting_req = NULL;
447
448 /* Run another pass to do progress */
449 Control_req_read_cb(req->fd, req);
450}
451
452int a_Control_init(void)
453{
454 int fd;
455 if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
456 MSG("cannot create control socket: %s\n", strerror(errno));
457 return -1;
458 }
459
460 struct sockaddr_un addr;
461 addr.sun_family = AF_UNIX;
462
463#define LEN ((int) sizeof(addr.sun_path))
464
465 char ctldir[LEN];
466 if (snprintf(ctldir, LEN, "%s/.dillo/ctl", dGethomedir()) >= LEN) {
467 MSG("control socket dir path too long: %s/.dillo/ctl\n", dGethomedir());
468 return -1;
469 }
470
471 /* Only the user should have access, otherwise other users could control the
472 * browser remotely */
473 if (mkdir(ctldir, 0700) != 0) {
474 if (errno != EEXIST) {
475 MSG("cannot create ctl dir: %s\n", strerror(errno));
476 return -1;
477 }
478 }
479
480 if (snprintf(addr.sun_path, LEN, "%s/%d", ctldir, (int) getpid()) >= LEN) {
481 MSG("control socket path too long: %s/%d\n", ctldir, (int) getpid());
482 return -1;
483 }
484
485 int slen = sizeof(addr);
486
487 if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
488 MSG("control socket fcntl failed: %s\n", strerror(errno));
489 return -1;
490 }
491
492 int on = 1;
493 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)) < 0) {
494 MSG("control socket setsockopt failed: %s\n", strerror(errno));
495 return -1;
496 }
497
498 if (ioctl(fd, FIONBIO, (char *)&on) != 0) {
499 MSG("control socket ioctl failed: %s\n", strerror(errno));
500 return -1;
501 }
502
503 if (bind(fd, (struct sockaddr *) &addr, slen) != 0) {
504 MSG("cannot bind control socket: %s\n", strerror(errno));
505 return -1;
506 }
507
508 if (listen(fd, 32) != 0) {
509 MSG("cannot listen control socket: %s\n", strerror(errno));
510 return -1;
511 }
512
513 control_fd = fd;
514 control_path = dStrdup(addr.sun_path);
515
516 a_IOwatch_add_fd(control_fd, DIO_READ, Control_listen_cb, NULL);
517
518 return 0;
519}
520
521int a_Control_free(void)
522{
523 /* Nothing to do */
524 if (control_fd == -1)
525 return 0;
526
527 assert(control_path);
528
529 a_IOwatch_remove_fd(control_fd, DIO_READ);
530 if (dClose(control_fd) != 0) {
531 MSG_ERR("close ctl socket failed\n");
532 return -1;
533 }
534 if (unlink(control_path) != 0) {
535 MSG_ERR("unlink ctl socket failed\n");
536 return -1;
537 }
538 dFree(control_path);
539 return 0;
540}
541
542#else
543
544int a_Control_init(void) { return 0; }
545int a_Control_is_waiting(void) { return 0; }
547int a_Control_free(void) { return 0; }
548
549#endif
#define _MSG(...)
Definition bookmarks.c:44
#define MSG(...)
Definition bookmarks.c:45
Dstr * a_Cache_get_header(const DilloUrl *Url)
Definition cache.c:682
void a_Cache_entry_inject(const DilloUrl *Url, const char *buf, size_t len, int flags)
Inject full page content directly into the cache.
Definition cache.c:292
#define CA_GotHeader
Definition cache.h:34
#define CA_GotLength
Definition cache.h:36
int a_Capi_get_buf(const DilloUrl *Url, char **PBuf, int *BufSize)
Get the cache's buffer for the URL, and its size.
Definition capi.c:543
void a_Control_notify_finish(BrowserWindow *bw)
Definition control.c:546
int a_Control_free(void)
Definition control.c:547
int a_Control_is_waiting(void)
Definition control.c:545
int a_Control_init(void)
Definition control.c:544
void * dMalloc0(size_t size)
Definition dlib.c:60
void dFree(void *mem)
Definition dlib.c:67
void dStr_sprintfa(Dstr *ds, const char *format,...)
Printf-like function that appends.
Definition dlib.c:463
char * dStrdup(const char *s)
Definition dlib.c:76
Dstr * dStr_sized_new(int sz)
Create a new string with a given size.
Definition dlib.c:253
void dStr_free(Dstr *ds, int all)
Free a dillo string.
Definition dlib.c:336
int dClose(int fd)
Close a FD handling EINTR.
Definition dlib.c:978
void dStr_append_l(Dstr *ds, const char *s, int l)
Append a C string to a Dstr (providing length).
Definition dlib.c:307
char * dGethomedir(void)
Return the home directory in a static string (don't free)
Definition dlib.c:933
#define dStrerror
Definition dlib.h:124
#define BUFSIZ
Definition dlib.h:64
#define MSG_ERR(...)
Definition dpid_common.h:23
#define LEN
Definition hyphenator.cc:31
void a_IOwatch_add_fd(int fd, int when, Fl_FD_Handler Callback, void *usr_data=0)
Hook a Callback for a certain activities in a FD.
Definition iowatch.cc:22
void a_IOwatch_remove_fd(int fd, int when)
Remove a Callback for a given FD (or just remove some events)
Definition iowatch.cc:32
#define DIO_READ
Definition iowatch.hh:7
#define DIO_WRITE
Definition iowatch.hh:8
Contains the specific data for a single window.
Definition bw.h:27
Definition url.h:88
Definition dlib.h:131
Dstr_char_t * str
Definition dlib.h:134
int len
Definition dlib.h:133
void a_Timeout_add(float t, TimeoutCb_t cb, void *cbdata)
Hook a one-time timeout function 'cb' after 't' seconds with 'cbdata" as its data.
Definition timeout.cc:26
void a_Timeout_actually_remove(TimeoutCb_t cb, void *data)
Definition timeout.cc:49
void a_UIcmd_set_msg(BrowserWindow *bw, const char *format,...)
Definition uicmd.cc:1636
const char * a_UIcmd_get_page_title(BrowserWindow *bw)
Definition uicmd.cc:1623
void a_UIcmd_reload_all_active()
Definition uicmd.cc:958
BrowserWindow * a_UIcmd_get_first_active_bw(void)
Definition uicmd.cc:943
int a_UIcmd_by_name(void *vbw, const char *cmd_name)
Definition uicmd.cc:1775
void a_UIcmd_close_all_bw(void *force)
Definition uicmd.cc:725
void a_UIcmd_open_urlstr(void *vbw, const char *urlstr)
Definition uicmd.cc:774
int a_UIcmd_has_finished(BrowserWindow *bw)
Definition uicmd.cc:1666
void a_UIcmd_repush(void *vbw)
Definition uicmd.cc:971
char * a_UIcmd_get_location_text(BrowserWindow *bw)
Definition uicmd.cc:1554
void a_Url_free(DilloUrl *url)
Free a DilloUrl.
Definition url.c:207
DilloUrl * a_Url_new(const char *url_str, const char *base_url)
Transform (and resolve) an URL string into the respective DilloURL.
Definition url.c:370