blob: 4a8370b9520d1f1bc7b4faadedc366956c4add14 [file] [log] [blame] [edit]
/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
* Copyright 2011 Pierre Ossman <ossman@cendio.se> for Cendio AB
* Copyright (C) 2011 D. R. Commander. All Rights Reserved.
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <locale.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#ifdef WIN32
#include <os/winerrno.h>
#include <direct.h>
#define mkdir(path, mode) _mkdir(path)
#endif
#if !defined(WIN32) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#endif
#include <rfb/Logger_stdio.h>
#include <rfb/SecurityClient.h>
#include <rfb/Security.h>
#ifdef HAVE_GNUTLS
#include <rfb/CSecurityTLS.h>
#endif
#include <rfb/LogWriter.h>
#include <rfb/Timer.h>
#include <rfb/Exception.h>
#include <network/TcpSocket.h>
#include <os/os.h>
#include <FL/Fl.H>
#include <FL/Fl_Widget.H>
#include <FL/Fl_PNG_Image.H>
#include <FL/Fl_Sys_Menu_Bar.H>
#include <FL/fl_ask.H>
#include <FL/x.H>
#include "i18n.h"
#include "parameters.h"
#include "CConn.h"
#include "ServerDialog.h"
#include "UserDialog.h"
#include "vncviewer.h"
#include "fltk_layout.h"
#ifdef WIN32
#include "resource.h"
#include "win32.h"
#endif
rfb::LogWriter vlog("main");
using namespace network;
using namespace rfb;
using namespace std;
char vncServerName[VNCSERVERNAMELEN] = { '\0' };
static const char *argv0 = NULL;
static bool exitMainloop = false;
static const char *exitError = NULL;
static const char *about_text()
{
static char buffer[1024];
// This is used in multiple places with potentially different
// encodings, so we need to make sure we get a fresh string every
// time.
snprintf(buffer, sizeof(buffer),
_("TigerVNC Viewer %d-bit v%s\n"
"Built on: %s\n"
"Copyright (C) 1999-%d TigerVNC Team and many others (see README.rst)\n"
"See https://www.tigervnc.org for information on TigerVNC."),
(int)sizeof(size_t)*8, PACKAGE_VERSION,
BUILD_TIMESTAMP, 2019);
return buffer;
}
void exit_vncviewer(const char *error)
{
// Prioritise the first error we get as that is probably the most
// relevant one.
if ((error != NULL) && (exitError == NULL))
exitError = strdup(error);
exitMainloop = true;
}
bool should_exit()
{
return exitMainloop;
}
void about_vncviewer()
{
fl_message_title(_("About TigerVNC Viewer"));
fl_message("%s", about_text());
}
void run_mainloop()
{
int next_timer;
next_timer = Timer::checkTimeouts();
if (next_timer == 0)
next_timer = INT_MAX;
if (Fl::wait((double)next_timer / 1000.0) < 0.0) {
vlog.error(_("Internal FLTK error. Exiting."));
exit(-1);
}
}
#ifdef __APPLE__
static void about_callback(Fl_Widget *widget, void *data)
{
about_vncviewer();
}
static void new_connection_cb(Fl_Widget *widget, void *data)
{
const char *argv[2];
pid_t pid;
pid = fork();
if (pid == -1) {
vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno));
return;
}
if (pid != 0)
return;
argv[0] = argv0;
argv[1] = NULL;
execvp(argv[0], (char * const *)argv);
vlog.error(_("Error starting new TigerVNC Viewer: %s"), strerror(errno));
_exit(1);
}
#endif
static void CleanupSignalHandler(int sig)
{
// CleanupSignalHandler allows C++ object cleanup to happen because it calls
// exit() rather than the default which is to abort.
vlog.info(_("Termination signal %d has been received. TigerVNC Viewer will now exit."), sig);
exit(1);
}
static void init_fltk()
{
// Basic text size (10pt @ 96 dpi => 13px)
FL_NORMAL_SIZE = 13;
// Select a FLTK scheme and background color that looks somewhat
// close to modern systems
Fl::scheme("gtk+");
Fl::background(220, 220, 220);
// macOS has a slightly brighter default background though
#ifdef __APPLE__
Fl::background(240, 240, 240);
#endif
// Proper Gnome Shell integration requires that we set a sensible
// WM_CLASS for the window.
Fl_Window::default_xclass("vncviewer");
// Set the default icon for all windows.
#ifdef WIN32
HICON lg, sm;
lg = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON),
IMAGE_ICON, GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON),
LR_DEFAULTCOLOR | LR_SHARED);
sm = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON),
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR | LR_SHARED);
Fl_Window::default_icons(lg, sm);
#elif ! defined(__APPLE__)
const int icon_sizes[] = {48, 32, 24, 16};
Fl_PNG_Image *icons[4];
int count;
count = 0;
// FIXME: Follow icon theme specification
for (size_t i = 0;i < sizeof(icon_sizes)/sizeof(icon_sizes[0]);i++) {
char icon_path[PATH_MAX];
bool exists;
sprintf(icon_path, "%s/icons/hicolor/%dx%d/apps/tigervnc.png",
DATA_DIR, icon_sizes[i], icon_sizes[i]);
#ifndef WIN32
struct stat st;
if (stat(icon_path, &st) != 0)
#else
struct _stat st;
if (_stat(icon_path, &st) != 0)
return(false);
#endif
exists = false;
else
exists = true;
if (exists) {
icons[count] = new Fl_PNG_Image(icon_path);
if (icons[count]->w() == 0 ||
icons[count]->h() == 0 ||
icons[count]->d() != 4) {
delete icons[count];
continue;
}
count++;
}
}
Fl_Window::default_icons((const Fl_RGB_Image**)icons, count);
for (int i = 0;i < count;i++)
delete icons[i];
#endif
// This makes the "icon" in dialogs rounded, which fits better
// with the above schemes.
fl_message_icon()->box(FL_UP_BOX);
// Turn off the annoying behaviour where popups track the mouse.
fl_message_hotspot(false);
// Avoid empty titles for popups
fl_message_title_default(_("TigerVNC Viewer"));
#ifdef WIN32
// Most "normal" Windows apps use this font for UI elements.
Fl::set_font(FL_HELVETICA, "Tahoma");
#endif
// FLTK exposes these so that we can translate them.
fl_no = _("No");
fl_yes = _("Yes");
fl_ok = _("OK");
fl_cancel = _("Cancel");
fl_close = _("Close");
#ifdef __APPLE__
/* Needs trailing space */
static char fltk_about[16];
snprintf(fltk_about, sizeof(fltk_about), "%s ", _("About"));
Fl_Mac_App_Menu::about = fltk_about;
static char fltk_hide[16];
snprintf(fltk_hide, sizeof(fltk_hide), "%s ", _("Hide"));
Fl_Mac_App_Menu::hide = fltk_hide;
static char fltk_quit[16];
snprintf(fltk_quit, sizeof(fltk_quit), "%s ", _("Quit"));
Fl_Mac_App_Menu::quit = fltk_quit;
Fl_Mac_App_Menu::print = ""; // Don't want the print item
Fl_Mac_App_Menu::services = _("Services");
Fl_Mac_App_Menu::hide_others = _("Hide Others");
Fl_Mac_App_Menu::show = _("Show All");
fl_mac_set_about(about_callback, NULL);
Fl_Sys_Menu_Bar *menubar;
char buffer[1024];
menubar = new Fl_Sys_Menu_Bar(0, 0, 500, 25);
// Fl_Sys_Menu_Bar overrides methods without them being virtual,
// which means we cannot use our generic Fl_Menu_ helpers.
if (fltk_menu_escape(p_("SysMenu|", "&File"),
buffer, sizeof(buffer)) < sizeof(buffer))
menubar->add(buffer, 0, 0, 0, FL_SUBMENU);
if (fltk_menu_escape(p_("SysMenu|File|", "&New Connection"),
buffer, sizeof(buffer)) < sizeof(buffer))
menubar->insert(1, buffer, FL_COMMAND | 'n', new_connection_cb);
#endif
}
static void mkvnchomedir()
{
// Create .vnc in the user's home directory if it doesn't already exist
char* homeDir = NULL;
if (getvnchomedir(&homeDir) == -1) {
vlog.error(_("Could not create VNC home directory: can't obtain home "
"directory path."));
} else {
int result = mkdir(homeDir, 0755);
if (result == -1 && errno != EEXIST)
vlog.error(_("Could not create VNC home directory: %s."), strerror(errno));
delete [] homeDir;
}
}
static void usage(const char *programName)
{
#ifdef WIN32
// If we don't have a console then we need to create one for output
if (GetConsoleWindow() == NULL) {
HANDLE handle;
int fd;
AllocConsole();
handle = GetStdHandle(STD_ERROR_HANDLE);
fd = _open_osfhandle((intptr_t)handle, O_TEXT);
*stderr = *fdopen(fd, "w");
}
#endif
fprintf(stderr,
"\n"
"usage: %s [parameters] [host][:displayNum]\n"
" %s [parameters] [host][::port]\n"
#ifndef WIN32
" %s [parameters] [unix socket]\n"
#endif
" %s [parameters] -listen [port]\n"
" %s [parameters] [.tigervnc file]\n",
programName, programName,
#ifndef WIN32
programName,
#endif
programName, programName);
#if !defined(WIN32) && !defined(__APPLE__)
fprintf(stderr,"\n"
"Options:\n\n"
" -display Xdisplay - Specifies the X display for the viewer window\n"
" -geometry geometry - Initial position of the main VNC viewer window. See the\n"
" man page for details.\n");
#endif
fprintf(stderr,"\n"
"Parameters can be turned on with -<param> or off with -<param>=0\n"
"Parameters which take a value can be specified as "
"-<param> <value>\n"
"Other valid forms are <param>=<value> -<param>=<value> "
"--<param>=<value>\n"
"Parameter names are case-insensitive. The parameters are:\n\n");
Configuration::listParams(79, 14);
#ifdef WIN32
// Just wait for the user to kill the console window
Sleep(INFINITE);
#endif
exit(1);
}
static void
potentiallyLoadConfigurationFile(char *vncServerName)
{
const bool hasPathSeparator = (strchr(vncServerName, '/') != NULL ||
(strchr(vncServerName, '\\')) != NULL);
if (hasPathSeparator) {
#ifndef WIN32
struct stat sb;
// This might be a UNIX socket, we need to check
if (stat(vncServerName, &sb) == -1) {
// Some access problem; let loadViewerParameters() deal with it...
} else {
if ((sb.st_mode & S_IFMT) == S_IFSOCK)
return;
}
#endif
try {
const char* newServerName;
newServerName = loadViewerParameters(vncServerName);
// This might be empty, but we still need to clear it so we
// don't try to connect to the filename
strncpy(vncServerName, newServerName, VNCSERVERNAMELEN-1);
vncServerName[VNCSERVERNAMELEN-1] = '\0';
} catch (rfb::Exception& e) {
vlog.error("%s", e.str());
if (alertOnFatalError)
fl_alert("%s", e.str());
exit(EXIT_FAILURE);
}
}
}
#ifndef WIN32
static int
interpretViaParam(char *remoteHost, int *remotePort, int localPort)
{
const int SERVER_PORT_OFFSET = 5900;
char *pos = strchr(vncServerName, ':');
if (pos == NULL)
*remotePort = SERVER_PORT_OFFSET;
else {
int portOffset = SERVER_PORT_OFFSET;
size_t len;
*pos++ = '\0';
len = strlen(pos);
if (*pos == ':') {
/* Two colons is an absolute port number, not an offset. */
pos++;
len--;
portOffset = 0;
}
if (!len || strspn (pos, "-0123456789") != len )
return 1;
*remotePort = atoi(pos) + portOffset;
}
if (*vncServerName != '\0')
strncpy(remoteHost, vncServerName, VNCSERVERNAMELEN);
else
strncpy(remoteHost, "localhost", VNCSERVERNAMELEN);
remoteHost[VNCSERVERNAMELEN - 1] = '\0';
snprintf(vncServerName, VNCSERVERNAMELEN, "localhost::%d", localPort);
vncServerName[VNCSERVERNAMELEN - 1] = '\0';
return 0;
}
static void
createTunnel(const char *gatewayHost, const char *remoteHost,
int remotePort, int localPort)
{
const char *cmd = getenv("VNC_VIA_CMD");
char *cmd2, *percent;
char lport[10], rport[10];
sprintf(lport, "%d", localPort);
sprintf(rport, "%d", remotePort);
setenv("G", gatewayHost, 1);
setenv("H", remoteHost, 1);
setenv("R", rport, 1);
setenv("L", lport, 1);
if (!cmd)
cmd = "/usr/bin/ssh -f -L \"$L\":\"$H\":\"$R\" \"$G\" sleep 20";
/* Compatibility with TigerVNC's method. */
cmd2 = strdup(cmd);
while ((percent = strchr(cmd2, '%')) != NULL)
*percent = '$';
system(cmd2);
free(cmd2);
}
static int mktunnel()
{
const char *gatewayHost;
char remoteHost[VNCSERVERNAMELEN];
int localPort = findFreeTcpPort();
int remotePort;
if (interpretViaParam(remoteHost, &remotePort, localPort) != 0)
return 1;
gatewayHost = (const char*)via;
createTunnel(gatewayHost, remoteHost, remotePort, localPort);
return 0;
}
#endif /* !WIN32 */
int main(int argc, char** argv)
{
UserDialog dlg;
argv0 = argv[0];
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE_NAME, LOCALE_DIR);
textdomain(PACKAGE_NAME);
rfb::SecurityClient::setDefaults();
// Write about text to console, still using normal locale codeset
fprintf(stderr,"\n%s\n", about_text());
// Set gettext codeset to what our GUI toolkit uses. Since we are
// passing strings from strerror/gai_strerror to the GUI, these must
// be in GUI codeset as well.
bind_textdomain_codeset(PACKAGE_NAME, "UTF-8");
bind_textdomain_codeset("libc", "UTF-8");
rfb::initStdIOLoggers();
#ifdef WIN32
rfb::initFileLogger("C:\\temp\\vncviewer.log");
#else
rfb::initFileLogger("/tmp/vncviewer.log");
#endif
rfb::LogWriter::setLogParams("*:stderr:30");
#ifdef SIGHUP
signal(SIGHUP, CleanupSignalHandler);
#endif
signal(SIGINT, CleanupSignalHandler);
signal(SIGTERM, CleanupSignalHandler);
init_fltk();
Configuration::enableViewerParams();
/* Load the default parameter settings */
char defaultServerName[VNCSERVERNAMELEN] = "";
try {
const char* configServerName;
configServerName = loadViewerParameters(NULL);
if (configServerName != NULL) {
strncpy(defaultServerName, configServerName, VNCSERVERNAMELEN-1);
defaultServerName[VNCSERVERNAMELEN-1] = '\0';
}
} catch (rfb::Exception& e) {
vlog.error("%s", e.str());
if (alertOnFatalError)
fl_alert("%s", e.str());
}
for (int i = 1; i < argc;) {
if (Configuration::setParam(argv[i])) {
i++;
continue;
}
if (argv[i][0] == '-') {
if (i+1 < argc) {
if (Configuration::setParam(&argv[i][1], argv[i+1])) {
i += 2;
continue;
}
}
usage(argv[0]);
}
strncpy(vncServerName, argv[i], VNCSERVERNAMELEN);
vncServerName[VNCSERVERNAMELEN - 1] = '\0';
i++;
}
// Check if the server name in reality is a configuration file
potentiallyLoadConfigurationFile(vncServerName);
mkvnchomedir();
#if !defined(WIN32) && !defined(__APPLE__)
if (strcmp(display, "") != 0) {
Fl::display(display);
}
fl_open_display();
XkbSetDetectableAutoRepeat(fl_display, True, NULL);
#endif
CSecurity::upg = &dlg;
#ifdef HAVE_GNUTLS
CSecurityTLS::msg = &dlg;
#endif
Socket *sock = NULL;
#ifndef WIN32
/* Specifying -via and -listen together is nonsense */
if (listenMode && strlen(via.getValueStr()) > 0) {
// TRANSLATORS: "Parameters" are command line arguments, or settings
// from a file or the Windows registry.
vlog.error(_("Parameters -listen and -via are incompatible"));
if (alertOnFatalError)
fl_alert(_("Parameters -listen and -via are incompatible"));
exit_vncviewer();
return 1;
}
#endif
if (listenMode) {
std::list<SocketListener*> listeners;
try {
int port = 5500;
if (isdigit(vncServerName[0]))
port = atoi(vncServerName);
createTcpListeners(&listeners, 0, port);
vlog.info(_("Listening on port %d"), port);
/* Wait for a connection */
while (sock == NULL) {
fd_set rfds;
FD_ZERO(&rfds);
for (std::list<SocketListener*>::iterator i = listeners.begin();
i != listeners.end();
i++)
FD_SET((*i)->getFd(), &rfds);
int n = select(FD_SETSIZE, &rfds, 0, 0, 0);
if (n < 0) {
if (errno == EINTR) {
vlog.debug("Interrupted select() system call");
continue;
} else {
throw rdr::SystemException("select", errno);
}
}
for (std::list<SocketListener*>::iterator i = listeners.begin ();
i != listeners.end();
i++)
if (FD_ISSET((*i)->getFd(), &rfds)) {
sock = (*i)->accept();
if (sock)
/* Got a connection */
break;
}
}
} catch (rdr::Exception& e) {
vlog.error("%s", e.str());
if (alertOnFatalError)
fl_alert("%s", e.str());
exit_vncviewer();
return 1;
}
while (!listeners.empty()) {
delete listeners.back();
listeners.pop_back();
}
} else {
if (vncServerName[0] == '\0') {
ServerDialog::run(defaultServerName, vncServerName);
if (vncServerName[0] == '\0')
return 1;
}
#ifndef WIN32
if (strlen (via.getValueStr()) > 0 && mktunnel() != 0)
usage(argv[0]);
#endif
}
CConn *cc = new CConn(vncServerName, sock);
while (!exitMainloop)
run_mainloop();
delete cc;
if (exitError != NULL && alertOnFatalError)
fl_alert("%s", exitError);
return 0;
}