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