1 /* ncdc - NCurses Direct Connect client
2
3 Copyright (c) 2011-2019 Yoran Heling
4
5 Permission is hereby granted, free of charge, to any person obtaining
6 a copy of this software and associated documentation files (the
7 "Software"), to deal in the Software without restriction, including
8 without limitation the rights to use, copy, modify, merge, publish,
9 distribute, sublicense, and/or sell copies of the Software, and to
10 permit persons to whom the Software is furnished to do so, subject to
11 the following conditions:
12
13 The above copyright notice and this permission notice shall be included
14 in all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 */
25
26
27 #include "ncdc.h"
28 #include "main.h"
29
30
31 // global variables
32
33 const char *main_version =
34 #include "version.h"
35 ;
36
37 GMainLoop *main_loop;
38
39
40 // input handling declarations
41
42 #if INTERFACE
43
44 // macros to operate on key values
45 #define INPT_KEY(code) (((guint64)0<<32) + (guint64)(code))
46 #define INPT_CHAR(code) (((guint64)1<<32) + (guint64)(code))
47 #define INPT_CTRL(code) (((guint64)2<<32) + (guint64)(code))
48 #define INPT_ALT(code) (((guint64)3<<32) + (guint64)(code))
49
50 #define INPT_CODE(key) ((gunichar)((key)&G_GUINT64_CONSTANT(0xFFFFFFFF)))
51 #define INPT_TYPE(key) ((char)((key)>>32))
52
53 #define KEY_ESCAPE (KEY_MAX+1)
54 #define KEY_BRACKETED_PASTE_START (KEY_ESCAPE+1)
55 #define KEY_BRACKETED_PASTE_END (KEY_BRACKETED_PASTE_START+1)
56
57 #endif
58
59 #define ctrl_to_ascii(x) ((x) == 127 ? '?' : g_ascii_tolower((x)+64))
60
handle_input()61 static void handle_input() {
62 /* Mapping from get_wch() to input_key_t:
63 * KEY_CODE_YES -> KEY(code)
64 * KEY_CODE_NO:
65 * char == 127 -> KEY(KEY_BACKSPACE)
66 * char <= 31 -> CTRL(char)
67 * !'^[' -> CHAR(char)
68 * ('^[', !) -> KEY(KEY_ESCAPE)
69 * ('^[', !CHAR) -> ignore both characters (1)
70 * ('^[', CHAR && '[') -> ignore both characters and the character after that (2)
71 * ('^[', CHAR && !'[') -> ALT(second char)
72 *
73 * 1. this is something like ctrl+alt+X, which we won't use
74 * 2. these codes indicate a 'Key' that somehow wasn't captured with
75 * KEY_CODE_YES. We won't attempt to interpret these ourselves.
76 *
77 * There are still several unhandled issues:
78 * - Ncurses does not catch all key codes, and there is no way of knowing how
79 * many bytes a key code spans. Things like ^[[1;3C won't be handled correctly. :-(
80 * - Ncurses can actually return key codes > KEY_MAX, but does not provide
81 * any mechanism for figuring out which key it actually was.
82 * - It may be useful to use define_key() for some special (and common) codes
83 * - Modifier keys will always be a problem. Most alt+key things work, except
84 * for those that may start a control code. alt+[ is a famous one, but
85 * there are others (like alt+O on my system). This is system-dependent,
86 * and again we have no way of knowing these things. (except perhaps by
87 * reading termcap entries on our own?)
88 */
89
90 guint64 key;
91 char buf[9];
92 int r;
93 wint_t code;
94 int lastesc = 0, curignore = 0;
95 while((r = get_wch(&code)) != ERR) {
96 if(curignore) {
97 curignore = 0;
98 continue;
99 }
100 // we use SIGWINCH, so KEY_RESIZE can be ignored
101 if(r == KEY_CODE_YES && code == KEY_RESIZE)
102 continue;
103 // backspace (outside of an escape sequence) is often sent as DEL control character, correct this
104 if(!lastesc && r != KEY_CODE_YES && code == 127) {
105 r = KEY_CODE_YES;
106 code = KEY_BACKSPACE;
107 }
108 // backspace inside an escape sequence is also possible, convert the other way around
109 if(lastesc && r == KEY_CODE_YES && code == KEY_BACKSPACE) {
110 r = !KEY_CODE_YES;
111 code = 127;
112 }
113 key = r == KEY_CODE_YES ? INPT_KEY(code) : code == 27 ? INPT_ALT(0) : code <= 31 ? INPT_CTRL(ctrl_to_ascii(code)) : INPT_CHAR(code);
114 // convert wchar_t into gunichar
115 if(INPT_TYPE(key) == 1) {
116 if((r = wctomb(buf, code)) < 0)
117 g_warning("Cannot encode character 0x%X", code);
118 buf[r] = 0;
119 key = INPT_CHAR(g_utf8_get_char_validated(buf, -1));
120 if(INPT_CODE(key) == (gunichar)-1 || INPT_CODE(key) == (gunichar)-2) {
121 g_warning("Invalid UTF-8 sequence in keyboard input. Are you sure you are running a UTF-8 locale?");
122 continue;
123 }
124 }
125 // check for escape sequence
126 if(lastesc) {
127 lastesc = 0;
128 if(INPT_TYPE(key) != 1)
129 continue;
130 if(INPT_CODE(key) == '[') {
131 curignore = 1;
132 continue;
133 }
134 key |= (guint64)3<<32; // a not very nice way of saying "turn this key into a INPT_ALT"
135 ui_input(key);
136 continue;
137 }
138 if(INPT_TYPE(key) == 3) {
139 lastesc = 1;
140 continue;
141 }
142 ui_input(key);
143 }
144 if(lastesc)
145 ui_input(INPT_KEY(KEY_ESCAPE));
146
147 ui_draw();
148 }
149
150
stdin_read(GIOChannel * src,GIOCondition cond,gpointer dat)151 static gboolean stdin_read(GIOChannel *src, GIOCondition cond, gpointer dat) {
152 handle_input();
153 return TRUE;
154 }
155
156
one_second_timer(gpointer dat)157 static gboolean one_second_timer(gpointer dat) {
158 // TODO: ratecalc_calc() requires fairly precise timing, perhaps do this in a separate thread?
159 ratecalc_calc();
160
161 // Detect day change
162 static char pday[11] = ""; // YYYY-MM-DD
163 char *cday = localtime_fmt("%F");
164 if(!pday[0])
165 strcpy(pday, cday);
166 if(strcmp(cday, pday) != 0) {
167 ui_daychange(cday);
168 strcpy(pday, cday);
169 }
170 g_free(cday);
171
172 // Disconnect offline users
173 cc_global_onlinecheck();
174
175 // And draw the UI
176 ui_draw();
177 return TRUE;
178 }
179
180
181 static gboolean screen_resized = FALSE;
182
screen_update_check(gpointer dat)183 static gboolean screen_update_check(gpointer dat) {
184 if(screen_resized) {
185 endwin();
186 doupdate();
187 ui_draw();
188 screen_resized = FALSE;
189 } else if(ui_checkupdate())
190 ui_draw();
191 return TRUE;
192 }
193
194
ncdc_quit()195 void ncdc_quit() {
196 g_main_loop_quit(main_loop);
197 }
198
199
ncdc_version()200 char *ncdc_version() {
201 static GString *ver = NULL;
202 static char *msg =
203 "%s %s (built %s %s)\n"
204 "Sendfile support: "
205 #ifdef HAVE_SENDFILE
206 "yes (%s)\n"
207 #else
208 "no\n"
209 #endif
210 "Libraries:\n"
211 " GLib %d.%d.%d (%d.%d.%d)\n"
212 " GnuTLS %s (%s)\n"
213 " SQLite %s (%s)"
214 #ifdef NCURSES_VERSION
215 "\n ncurses %s"
216 #endif
217 ;
218 if(ver)
219 return ver->str;
220 ver = g_string_new("");
221 g_string_printf(ver, msg, PACKAGE_NAME, main_version,
222 __DATE__, __TIME__,
223 #ifdef HAVE_LINUX_SENDFILE
224 "Linux",
225 #elif HAVE_BSD_SENDFILE
226 "BSD",
227 #endif
228 GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION, GLIB_MICRO_VERSION, glib_major_version, glib_minor_version, glib_micro_version,
229 GNUTLS_VERSION, gnutls_check_version(NULL),
230 SQLITE_VERSION, sqlite3_libversion()
231 #ifdef NCURSES_VERSION
232 , NCURSES_VERSION
233 #endif
234 );
235 return ver->str;
236 }
237
238
239 static FILE *stderrlog;
240
241 // redirect all non-fatal errors to the log
log_redirect(const gchar * dom,GLogLevelFlags level,const gchar * msg,gpointer dat)242 static void log_redirect(const gchar *dom, GLogLevelFlags level, const gchar *msg, gpointer dat) {
243 if(!(level & (G_LOG_LEVEL_INFO|G_LOG_LEVEL_DEBUG)) || (stderrlog != stderr && var_log_debug)) {
244 char *ts = localtime_fmt("[%F %H:%M:%S %Z]");
245 fprintf(stderrlog, "%s *%s* %s\n", ts, loglevel_to_str(level), msg);
246 g_free(ts);
247 fflush(stderrlog);
248 }
249 }
250
251
252 // clean-up our ncurses window before throwing a fatal error
log_fatal(const gchar * dom,GLogLevelFlags level,const gchar * msg,gpointer dat)253 static void log_fatal(const gchar *dom, GLogLevelFlags level, const gchar *msg, gpointer dat) {
254 endwin();
255 // print to both log file and stdout
256 if(stderrlog != stderr) {
257 fprintf(stderrlog, "\n\n*%s* %s\n", loglevel_to_str(level), msg);
258 fflush(stderrlog);
259 }
260 printf("\n\n*%s* %s\n", loglevel_to_str(level), msg);
261 }
262
263
open_autoconnect()264 static void open_autoconnect() {
265 char **hubs = db_vars_hubs();
266 char **hub;
267 // TODO: make sure the tabs are opened in the same order as they were in the last run?
268 for(hub=hubs; *hub; hub++)
269 if(var_get_bool(db_vars_hubid(*hub), VAR_autoconnect))
270 ui_tab_open(uit_hub_create(*hub+1, TRUE), FALSE, NULL);
271 listen_refresh();
272 g_strfreev(hubs);
273 }
274
275
276
277
278 // Fired when the screen is resized. Normally I would check for KEY_RESIZE,
279 // but that doesn't work very nicely together with select(). See
280 // http://www.webservertalk.com/archive107-2005-1-896232.html
281 // Also note that this is a signal handler, and all functions we call here must
282 // be re-entrant. Obviously none of the ncurses functions are, so let's set a
283 // variable and handle it in the screen_update_check_timer.
catch_sigwinch(int sig)284 static void catch_sigwinch(int sig) {
285 screen_resized = TRUE;
286 }
287
catch_sigpipe(int sig)288 static void catch_sigpipe(int sig) {
289 // Ignore.
290 }
291
292
293
294 // A special GSource to handle SIGTERM, SIGHUP and SIGUSR1 synchronously in the
295 // main thread. This is done because the functions to control the glib event
296 // loop are not re-entrant, and therefore cannot be called from signal
297 // handlers.
298
299 static gboolean main_sig_log = FALSE;
300 static gboolean main_sig_quit = FALSE;
301 static gboolean main_noterm = FALSE;
302
catch_sigterm(int sig)303 static void catch_sigterm(int sig) {
304 main_sig_quit = TRUE;
305 }
306
catch_sighup(int sig)307 static void catch_sighup(int sig) {
308 main_sig_quit = TRUE;
309 main_noterm = TRUE;
310 }
311
312 // Re-open the log files when receiving SIGUSR1.
catch_sigusr1(int sig)313 static void catch_sigusr1(int sig) {
314 main_sig_log = TRUE;
315 }
316
sighandle_prepare(GSource * source,gint * timeout)317 static gboolean sighandle_prepare(GSource *source, gint *timeout) {
318 *timeout = -1;
319 return main_sig_quit || main_sig_log;
320 }
321
sighandle_check(GSource * source)322 static gboolean sighandle_check(GSource *source) {
323 return main_sig_quit || main_sig_log;
324 }
325
sighandle_dispatch(GSource * source,GSourceFunc callback,gpointer user_data)326 static gboolean sighandle_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) {
327 return callback(NULL);
328 }
329
330 static GSourceFuncs sighandle_funcs = {
331 sighandle_prepare,
332 sighandle_check,
333 sighandle_dispatch,
334 NULL
335 };
336
sighandle_sourcefunc(gpointer dat)337 static gboolean sighandle_sourcefunc(gpointer dat) {
338 if(main_sig_quit) {
339 g_debug("%s received, terminating main loop.", main_noterm ? "SIGHUP" : "SIGTERM");
340 ncdc_quit();
341 main_sig_quit = FALSE;
342 }
343 if(main_sig_log) {
344 logfile_global_reopen();
345 main_sig_log = FALSE;
346 }
347 return TRUE;
348 }
349
350
351
352
353 // Commandline options
354
print_version(const gchar * name,const gchar * val,gpointer dat,GError ** err)355 static gboolean print_version(const gchar *name, const gchar *val, gpointer dat, GError **err) {
356 puts(ncdc_version());
357 exit(0);
358 }
359
360
361 static gboolean auto_open = TRUE;
362 static gboolean bracketed_paste = TRUE;
363
364 static GOptionEntry cli_options[] = {
365 { "version", 'v', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, print_version,
366 "Print version and compilation information.", NULL },
367 { "session-dir", 'c', 0, G_OPTION_ARG_FILENAME, &db_dir,
368 "Use a different session directory. Default: `$NCDC_DIR' or `$HOME/.ncdc'.", "<dir>" },
369 { "no-autoconnect", 'n', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &auto_open,
370 "Don't automatically connect to hubs with the `autoconnect' option set.", NULL },
371 { "no-bracketed-paste", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &bracketed_paste,
372 "Disable bracketed pasting.", NULL },
373 { NULL }
374 };
375
376
377
378
379 #ifdef USE_GCRYPT
380 /* These hacks with static exist to trick makeheaders into not copying the
381 * gcrypt macro into the header file. It does cause some of the functions to be
382 * exported, but they're pretty unique anyway. */
383 #define static
384 static GCRY_THREAD_OPTION_PTHREAD_IMPL;
385 #undef static
386 #endif
387
init_crypt()388 static void init_crypt() {
389 #ifdef USE_GCRYPT
390 gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread);
391 if(!gcry_check_version(GCRYPT_VERSION)) {
392 fputs("libgcrypt version mismatch\n", stderr);
393 exit(1);
394 }
395 gcry_control(GCRYCTL_ENABLE_QUICK_RANDOM);
396 gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
397 #endif
398 gnutls_global_init();
399 }
400
401
main(int argc,char ** argv)402 int main(int argc, char **argv) {
403 setlocale(LC_ALL, "");
404 // Early logging goes to stderr
405 stderrlog = stderr;
406
407 // parse commandline options
408 GOptionContext *optx = g_option_context_new("- NCurses Direct Connect");
409 g_option_context_add_main_entries(optx, cli_options, NULL);
410 GError *err = NULL;
411 if(!g_option_context_parse(optx, &argc, &argv, &err)) {
412 puts(err->message);
413 exit(1);
414 }
415 g_option_context_free(optx);
416
417 // check that the current locale is UTF-8. Things aren't going to work otherwise
418 if(!g_get_charset(NULL)) {
419 puts("WARNING: Your current locale is not set to UTF-8.");
420 puts("Non-ASCII characters may not display correctly.");
421 puts("Hit Ctrl+c to abort ncdc, or the return key to continue anyway.");
422 getchar();
423 }
424
425 // init stuff
426 init_crypt();
427 g_thread_init(NULL);
428
429 // Create main loop
430 main_loop = g_main_loop_new(NULL, FALSE);
431
432 // setup logging
433 g_log_set_handler(NULL, G_LOG_FATAL_MASK | G_LOG_FLAG_FATAL | G_LOG_LEVEL_ERROR, log_fatal, NULL);
434 g_log_set_default_handler(log_redirect, NULL);
435
436 // Init database & variables
437 db_init();
438 vars_init();
439
440 // open log file
441 char *errlog = g_build_filename(db_dir, "stderr.log", NULL);
442 if(!(stderrlog = fopen(errlog, "w"))) {
443 fprintf(stderr, "ERROR: Couldn't open %s for writing: %s\n", errlog, strerror(errno));
444 exit(1);
445 }
446 g_free(errlog);
447
448 // Init more stuff
449 hub_init_global();
450 net_init_global();
451 listen_global_init();
452 cc_global_init();
453 dl_init_global();
454 ui_cmdhist_init("history");
455 ui_init(bracketed_paste);
456 geoip_reinit(4);
457 geoip_reinit(6);
458
459 // setup SIGWINCH
460 struct sigaction act;
461 sigemptyset(&act.sa_mask);
462 act.sa_flags = SA_RESTART;
463 act.sa_handler = catch_sigwinch;
464 if(sigaction(SIGWINCH, &act, NULL) < 0)
465 g_error("Can't setup SIGWINCH: %s", g_strerror(errno));
466
467 // setup SIGTERM
468 act.sa_handler = catch_sigterm;
469 if(sigaction(SIGTERM, &act, NULL) < 0)
470 g_error("Can't setup SIGTERM: %s", g_strerror(errno));
471
472 // setup SIGHUP
473 act.sa_handler = catch_sighup;
474 if(sigaction(SIGHUP, &act, NULL) < 0)
475 g_error("Can't setup SIGHUP: %s", g_strerror(errno));
476
477 // setup SIGUSR1
478 act.sa_handler = catch_sigusr1;
479 if(sigaction(SIGUSR1, &act, NULL) < 0)
480 g_error("Can't setup SIGUSR1: %s", g_strerror(errno));
481
482 // setup SIGPIPE
483 act.sa_handler = catch_sigpipe;
484 if(sigaction(SIGPIPE, &act, NULL) < 0)
485 g_error("Can't setup SIGPIPE: %s", g_strerror(errno));
486
487 fl_init();
488 if(auto_open)
489 open_autoconnect();
490
491 // add some watches and start the main loop
492 GIOChannel *in = g_io_channel_unix_new(STDIN_FILENO);
493 g_io_add_watch(in, G_IO_IN, stdin_read, NULL);
494
495 GSource *sighandle = g_source_new(&sighandle_funcs, sizeof(GSource));
496 g_source_set_priority(sighandle, G_PRIORITY_HIGH);
497 g_source_set_callback(sighandle, sighandle_sourcefunc, NULL, NULL);
498 g_source_attach(sighandle, NULL);
499 g_source_unref(sighandle);
500
501 g_timeout_add_seconds_full(G_PRIORITY_HIGH, 1, one_second_timer, NULL, NULL);
502 g_timeout_add(100, screen_update_check, NULL);
503 int maxage = var_get_int(0, VAR_filelist_maxage);
504 g_timeout_add_seconds_full(G_PRIORITY_LOW, CLAMP(maxage, 3600, 24*3600), dl_fl_clean, NULL, NULL);
505
506 g_main_loop_run(main_loop);
507
508 // cleanup
509 if(!main_noterm) {
510 erase();
511 refresh();
512 endwin();
513 if(bracketed_paste)
514 ui_set_bracketed_paste(0);
515
516 printf("Flushing unsaved data to disk...");
517 fflush(stdout);
518 }
519 ui_cmdhist_close();
520 cc_global_close();
521 fl_flush(NULL);
522 dl_close_global();
523 db_close();
524 gnutls_global_deinit();
525 if(!main_noterm)
526 printf(" Done!\n");
527
528 g_debug("Clean shutdown.");
529 return 0;
530 }
531
532