dlg_stdcheckbox_handler,
I(offsetof(Config,try_ki_auth)));
-#ifndef NO_GSSAPI
- ctrl_checkbox(s, "Attempt GSSAPI auth (SSH-2)",
- NO_SHORTCUT, HELPCTX(no_help),
- dlg_stdcheckbox_handler,
- I(offsetof(Config,try_gssapi_auth)));
-#endif
-
s = ctrl_getset(b, "Connection/SSH/Auth", "params",
"Authentication parameters");
ctrl_checkbox(s, "Allow agent forwarding", 'f',
HELPCTX(ssh_auth_changeuser),
dlg_stdcheckbox_handler,
I(offsetof(Config,change_username)));
-#ifndef NO_GSSAPI
- ctrl_checkbox(s, "Allow GSSAPI credential delegation in SSH-2", NO_SHORTCUT,
- HELPCTX(no_help),
- dlg_stdcheckbox_handler,
- I(offsetof(Config,gssapifwd)));
-#endif
ctrl_filesel(s, "Private key file for authentication:", 'k',
FILTER_KEY_FILES, FALSE, "Select private key file",
HELPCTX(ssh_auth_privkey),
dlg_stdfilesel_handler, I(offsetof(Config, keyfile)));
#ifndef NO_GSSAPI
+ /*
+ * Connection/SSH/Auth/GSSAPI, which sadly won't fit on
+ * the main Auth panel.
+ */
+ ctrl_settitle(b, "Connection/SSH/Auth/GSSAPI",
+ "Options controlling GSSAPI authentication");
+ s = ctrl_getset(b, "Connection/SSH/Auth/GSSAPI", "gssapi", NULL);
+
+ ctrl_checkbox(s, "Attempt GSSAPI authentication (SSH-2 only)",
+ NO_SHORTCUT, HELPCTX(ssh_gssapi),
+ dlg_stdcheckbox_handler,
+ I(offsetof(Config,try_gssapi_auth)));
+
+ ctrl_checkbox(s, "Allow GSSAPI credential delegation", NO_SHORTCUT,
+ HELPCTX(ssh_gssapi_delegation),
+ dlg_stdcheckbox_handler,
+ I(offsetof(Config,gssapifwd)));
+
/*
* GSSAPI library selection.
*/
if (ngsslibs > 1) {
c = ctrl_draglist(s, "Preference order for GSSAPI libraries:", NO_SHORTCUT,
- HELPCTX(no_help),
+ HELPCTX(ssh_gssapi_libraries),
gsslist_handler, P(NULL));
c->listbox.height = ngsslibs;
+
+ /*
+ * I currently assume that if more than one GSS
+ * library option is available, then one of them is
+ * 'user-supplied' and so we should present the
+ * following file selector. This is at least half-
+ * reasonable, because if we're using statically
+ * linked GSSAPI then there will only be one option
+ * and no way to load from a user-supplied library,
+ * whereas if we're using dynamic libraries then
+ * there will almost certainly be some default
+ * option in addition to a user-supplied path. If
+ * anyone ever ports PuTTY to a system on which
+ * dynamic-library GSSAPI is available but there is
+ * absolutely no consensus on where to keep the
+ * libraries, there'll need to be a flag alongside
+ * ngsslibs to control whether the file selector is
+ * displayed.
+ */
+
+ ctrl_filesel(s, "User-supplied GSSAPI library path:", 'l',
+ FILTER_DYNLIB_FILES, FALSE, "Select library file",
+ HELPCTX(ssh_gssapi_libraries),
+ dlg_stdfilesel_handler,
+ I(offsetof(Config, ssh_gss_custom)));
}
#endif
}
that key, and ignore any other keys Pageant may have. If that fails,
PuTTY will ask for a passphrase as normal.
+\H{config-ssh-auth-gssapi} The GSSAPI panel
+
+\cfg{winhelp-topic}{ssh.auth.gssapi}
+
+The \q{GSSAPI} subpanel of the \q{Auth} panel controls the use of
+GSSAPI authentication. This is a mechanism which delegates the
+authentication exchange to a library elsewhere on the client
+machine, which in principle can authenticate in many different ways
+but in practice is usually used with the Kerberos single-sign-on
+protocol.
+
+GSSAPI is only available in the SSH-2 protocol.
+
+The topmost control on the GSSAPI subpanel is the checkbox labelled
+\q{Attempt GSSAPI authentication}. If this is disabled, GSSAPI will
+not be attempted at all and the rest of this panel is unused. If it
+is enabled, GSSAPI authentication will be attempted, and (typically)
+if your client machine has valid Kerberos credentials loaded, then
+PuTTY should be able to authenticate automatically to servers that
+support Kerberos logins.
+
+\S{config-ssh-auth-gssapi-delegation} \q{Allow GSSAPI credential
+delegation}
+
+\cfg{winhelp-topic}{ssh.auth.gssapi.delegation}
+
+GSSAPI credential delegation is a mechanism for passing on your
+Kerberos (or other) identity to the session on the SSH server. If
+you enable this option, then not only will PuTTY be able to log in
+automatically to a server that accepts your Kerberos credentials,
+but also you will be able to connect out from that server to other
+Kerberos-supporting services and use the same credentials just as
+automatically.
+
+(This option is the Kerberos analogue of SSH agent forwarding; see
+\k{pageant-forward} for some information on that.)
+
+Note that, like SSH agent forwarding, there is a security
+implication in the use of this option: the administrator of the
+server you connect to, or anyone else who has cracked the
+administrator account on that server, could fake your identity when
+connecting to further Kerberos-supporting services. However,
+Kerberos sites are typically run by a central authority, so the
+administrator of one server is likely to already have access to the
+other services too; so this would typically be less of a risk than
+SSH agent forwarding.
+
+\S{config-ssh-auth-gssapi-libraries} Preference order for GSSAPI
+libraries
+
+\cfg{winhelp-topic}{ssh.auth.gssapi.libraries}
+
+GSSAPI is a mechanism which allows more than one authentication
+method to be accessed through the same interface. Therefore, more
+than one authentication library may exist on your system which can
+be accessed using GSSAPI.
+
+PuTTY contains native support for a few well-known such libraries,
+and will look for all of them on your system and use whichever it
+finds. If more than one exists on your system and you need to use a
+specific one, you can adjust the order in which it will search using
+this preference list control.
+
+One of the options in the preference list is to use a user-specified
+GSSAPI library. If the library you want to use is not mentioned by
+name in PuTTY's list of options, you can enter its full pathname in
+the \q{User-supplied GSSAPI library path} field, and move the
+\q{User-supplied GSSAPI library} option in the preference list to
+make sure it is selected before anything else.
+
\H{config-ssh-tty} The TTY panel
The TTY panel lets you configure the remote pseudo-terminal.
int try_gssapi_auth; /* attempt gssapi auth */
int gssapifwd; /* forward tgt via gss */
int ssh_gsslist[4]; /* preference order for local GSS libs */
+ Filename ssh_gss_custom;
int ssh_subsys; /* run a subsystem rather than a command */
int ssh_subsys2; /* fallback to go with remote_cmd_ptr2 */
int ssh_no_shell; /* avoid running a shell */
write_setting_i(sesskey, "AuthTIS", cfg->try_tis_auth);
write_setting_i(sesskey, "AuthKI", cfg->try_ki_auth);
write_setting_i(sesskey, "AuthGSSAPI", cfg->try_gssapi_auth);
+#ifndef NO_GSSAPI
wprefs(sesskey, "GSSLibs", gsslibkeywords, ngsslibs,
cfg->ssh_gsslist);
+ write_setting_filename(sesskey, "GSSCustom", cfg->ssh_gss_custom);
+#endif
write_setting_i(sesskey, "SshNoShell", cfg->ssh_no_shell);
write_setting_i(sesskey, "SshProt", cfg->sshprot);
write_setting_s(sesskey, "LogHost", cfg->loghost);
gppi(sesskey, "AuthTIS", 0, &cfg->try_tis_auth);
gppi(sesskey, "AuthKI", 1, &cfg->try_ki_auth);
gppi(sesskey, "AuthGSSAPI", 1, &cfg->try_gssapi_auth);
+#ifndef NO_GSSAPI
gprefs(sesskey, "GSSLibs", "\0",
gsslibkeywords, ngsslibs, cfg->ssh_gsslist);
+ gppfile(sesskey, "GSSCustom", &cfg->ssh_gss_custom);
+#endif
gppi(sesskey, "SshNoShell", 0, &cfg->ssh_no_shell);
gppfile(sesskey, "PublicKeyFile", &cfg->keyfile);
gpps(sesskey, "RemoteCommand", "", cfg->remote_cmd,
* Fully qualified host name, which we need if doing GSSAPI.
*/
char *fullhostname;
+
+#ifndef NO_GSSAPI
+ /*
+ * GSSAPI libraries for this session.
+ */
+ struct ssh_gss_liblist *gsslibs;
+#endif
};
#define logevent(s) logevent(ssh->frontend, s)
in_commasep_string("password", methods, methlen);
s->can_keyb_inter = ssh->cfg.try_ki_auth &&
in_commasep_string("keyboard-interactive", methods, methlen);
-#ifndef NO_GSSAPI
- ssh_gss_init();
+#ifndef NO_GSSAPI
+ if (!ssh->gsslibs)
+ ssh->gsslibs = ssh_gss_setup(&ssh->cfg);
s->can_gssapi = ssh->cfg.try_gssapi_auth &&
in_commasep_string("gssapi-with-mic", methods, methlen) &&
- n_ssh_gss_libraries > 0;
+ ssh->gsslibs->nlibraries > 0;
#endif
}
s->gsslib = NULL;
for (i = 0; i < ngsslibs; i++) {
int want_id = ssh->cfg.ssh_gsslist[i];
- for (j = 0; j < n_ssh_gss_libraries; j++)
- if (ssh_gss_libraries[j].id == want_id) {
- s->gsslib = &ssh_gss_libraries[j];
+ for (j = 0; j < ssh->gsslibs->nlibraries; j++)
+ if (ssh->gsslibs->libraries[j].id == want_id) {
+ s->gsslib = &ssh->gsslibs->libraries[j];
goto got_gsslib; /* double break */
}
}
ssh->max_data_size = parse_blocksize(ssh->cfg.ssh_rekey_data);
ssh->kex_in_progress = FALSE;
+#ifndef NO_GSSAPI
+ ssh->gsslibs = NULL;
+#endif
+
p = connect_to_host(ssh, host, port, realhost, nodelay, keepalive);
if (p != NULL)
return p;
if (ssh->pinger)
pinger_free(ssh->pinger);
bufchain_clear(&ssh->queued_incoming_data);
+#ifndef NO_GSSAPI
+ if (ssh->gsslibs)
+ ssh_gss_cleanup(ssh->gsslibs);
+#endif
sfree(ssh);
random_unref();
struct ssh_gss_library;
/*
- * Do startup-time initialisation for using GSSAPI. This should
- * correctly initialise the array of struct ssh_gss_library declared
- * below.
+ * Prepare a collection of GSSAPI libraries for use in a single SSH
+ * connection. Returns a structure containing a list of libraries,
+ * with their ids (see struct ssh_gss_library below) filled in so
+ * that the client can go through them in the SSH user's preferred
+ * order.
*
- * Must be callable multiple times (since the most convenient place
- * to call it _from_ is the ssh.c setup code), and should harmlessly
- * return success if already initialised.
+ * Must always return non-NULL. (Even if no libraries are available,
+ * it must return an empty structure.)
+ *
+ * The free function cleans up the structure, and its associated
+ * libraries (if any).
*/
-void ssh_gss_init(void);
+struct ssh_gss_liblist {
+ struct ssh_gss_library *libraries;
+ int nlibraries;
+};
+struct ssh_gss_liblist *ssh_gss_setup(const Config *cfg);
+void ssh_gss_cleanup(struct ssh_gss_liblist *list);
/*
* Fills in buf with a string describing the GSSAPI mechanism in
* be more than one set of them available.
*/
} u;
-};
-extern struct ssh_gss_library ssh_gss_libraries[];
-extern int n_ssh_gss_libraries;
+ /*
+ * Wrapper layers will often also need to store a library handle
+ * of some sort for cleanup time.
+ */
+ void *handle;
+};
#endif /* NO_GSSAPI */
/* For platforms not supporting GSSAPI */
-void ssh_gss_init(void)
+struct ssh_gss_liblist *ssh_gss_setup(const Config *cfg)
{
+ struct ssh_gss_liblist *list = snew(struct ssh_gss_liblist *);
+ list->libraries = NULL;
+ list->nlibraries = 0;
+ return list;
+}
+
+void ssh_gss_cleanup(struct ssh_gss_liblist *list)
+{
+ sfree(list);
}
#endif /* NO_GSSAPI */
*/
#define HELPCTX(x) P(NULL)
#define FILTER_KEY_FILES NULL /* FIXME */
+#define FILTER_DYNLIB_FILES NULL /* FIXME */
/*
* Under X, selection data must not be NUL-terminated.
/* Unix code to set up the GSSAPI library list. */
-struct ssh_gss_library ssh_gss_libraries[3];
-int n_ssh_gss_libraries = 0;
-static int initialised = FALSE;
+#if !defined NO_LIBDL && !defined NO_GSSAPI
-const int ngsslibs = 3;
-const char *const gsslibnames[3] = {
+const int ngsslibs = 4;
+const char *const gsslibnames[4] = {
"libgssapi (Heimdal)",
"libgssapi_krb5 (MIT Kerberos)",
"libgss (Sun)",
+ "User-specified GSSAPI library",
};
const struct keyval gsslibkeywords[] = {
{ "libgssapi", 0 },
{ "libgssapi_krb5", 1 },
{ "libgss", 2 },
+ { "custom", 3 },
};
-#ifndef NO_LIBDL
-
/*
* Run-time binding against a choice of GSSAPI implementations. We
* try loading several libraries, and produce an entry in
{
lib->id = id;
lib->gsslogmsg = msg;
+ lib->handle = dlhandle;
#define BIND_GSS_FN(name) \
lib->u.gssapi.name = (t_gss_##name) dlsym(dlhandle, "gss_" #name)
}
/* Dynamically load gssapi libs. */
-void ssh_gss_init(void)
+struct ssh_gss_liblist *ssh_gss_setup(const Config *cfg)
{
void *gsslib;
+ struct ssh_gss_liblist *list = snew(struct ssh_gss_liblist);
- if (initialised) return;
- initialised = TRUE;
+ list->libraries = snewn(4, struct ssh_gss_library);
+ list->nlibraries = 0;
/* Heimdal's GSSAPI Library */
if ((gsslib = dlopen("libgssapi.so.2", RTLD_LAZY)) != NULL)
- gss_init(&ssh_gss_libraries[n_ssh_gss_libraries++], gsslib,
+ gss_init(&list->libraries[list->nlibraries++], gsslib,
0, "Using GSSAPI from libgssapi.so.2");
/* MIT Kerberos's GSSAPI Library */
if ((gsslib = dlopen("libgssapi_krb5.so.2", RTLD_LAZY)) != NULL)
- gss_init(&ssh_gss_libraries[n_ssh_gss_libraries++], gsslib,
+ gss_init(&list->libraries[list->nlibraries++], gsslib,
1, "Using GSSAPI from libgssapi_krb5.so.2");
/* Sun's GSSAPI Library */
if ((gsslib = dlopen("libgss.so.1", RTLD_LAZY)) != NULL)
- gss_init(&ssh_gss_libraries[n_ssh_gss_libraries++], gsslib,
+ gss_init(&list->libraries[list->nlibraries++], gsslib,
2, "Using GSSAPI from libgss.so.1");
+
+ /* User-specified GSSAPI library */
+ if (cfg->ssh_gss_custom.path[0] &&
+ (gsslib = dlopen(cfg->ssh_gss_custom.path, RTLD_LAZY)) != NULL)
+ gss_init(&list->libraries[list->nlibraries++], gsslib,
+ 3, dupprintf("Using GSSAPI from user-specified"
+ " library '%s'", cfg->ssh_gss_custom.path));
+
+ return list;
+}
+
+void ssh_gss_cleanup(struct ssh_gss_liblist *list)
+{
+ int i;
+
+ /*
+ * dlopen and dlclose are defined to employ reference counting
+ * in the case where the same library is repeatedly dlopened, so
+ * even in a multiple-sessions-per-process context it's safe to
+ * naively dlclose everything here without worrying about
+ * destroying it under the feet of another SSH instance still
+ * using it.
+ */
+ for (i = 0; i < list->nlibraries; i++) {
+ dlclose(list->libraries[i].handle);
+ if (list->libraries[i].id == 3) {
+ /* The 'custom' id involves a dynamically allocated message.
+ * Note that we must cast away the 'const' to free it. */
+ sfree((char *)list->libraries[i].gsslogmsg);
+ }
+ }
+ sfree(list->libraries);
+ sfree(list);
}
-#else /* NO_LIBDL */
+#elif !defined NO_GSSAPI
+
+const int ngsslibs = 1;
+const char *const gsslibnames[1] = {
+ "static",
+};
+const struct keyval gsslibkeywords[] = {
+ { "static", 0 },
+};
/*
* Link-time binding against GSSAPI. Here we just construct a single
#include <gssapi/gssapi.h>
/* Dynamically load gssapi libs. */
-void ssh_gss_init(void)
+struct ssh_gss_liblist *ssh_gss_setup(const Config *cfg)
{
- if (initialised) return;
- initialised = TRUE;
+ struct ssh_gss_liblist *list = snew(struct ssh_gss_liblist);
- n_ssh_gss_libraries = 1;
- ssh_gss_libraries[0].gsslogmsg = "Using statically linked GSSAPI";
+ list->libraries = snew(struct ssh_gss_library);
+ list->nlibraries = 1;
+
+ list->libraries[0].gsslogmsg = "Using statically linked GSSAPI";
#define BIND_GSS_FN(name) \
- ssh_gss_libraries[0].u.gssapi.name = (t_gss_##name) gss_##name
+ list->libraries[0].u.gssapi.name = (t_gss_##name) gss_##name
BIND_GSS_FN(delete_sec_context);
BIND_GSS_FN(display_status);
#undef BIND_GSS_FN
- ssh_gssapi_bind_fns(&ssh_gss_libraries[0]);
+ ssh_gssapi_bind_fns(&list->libraries[0]);
+
+ return list;
+}
+
+void ssh_gss_cleanup(struct ssh_gss_liblist *list)
+{
+ sfree(list->libraries);
+ sfree(list);
}
#endif /* NO_LIBDL */
/* Windows code to set up the GSSAPI library list. */
-struct ssh_gss_library ssh_gss_libraries[2];
-int n_ssh_gss_libraries = 0;
-static int initialised = FALSE;
-
-const int ngsslibs = 2;
-const char *const gsslibnames[2] = {
- "GSSAPI32.DLL (MIT Kerberos)",
- "SECUR32.DLL (Microsoft SSPI)",
+const int ngsslibs = 3;
+const char *const gsslibnames[3] = {
+ "MIT Kerberos GSSAPI32.DLL",
+ "Microsoft SSPI SECUR32.DLL",
+ "User-specified GSSAPI DLL",
};
const struct keyval gsslibkeywords[] = {
{ "gssapi32", 0 },
{ "sspi", 1 },
+ { "custom", 2 },
};
DECL_WINDOWS_FUNCTION(static, SECURITY_STATUS,
static void ssh_sspi_bind_fns(struct ssh_gss_library *lib);
-void ssh_gss_init(void)
+struct ssh_gss_liblist *ssh_gss_setup(const Config *cfg)
{
HMODULE module;
+ HKEY regkey;
+ struct ssh_gss_liblist *list = snew(struct ssh_gss_liblist);
- if (initialised) return;
- initialised = TRUE;
+ list->libraries = snewn(3, struct ssh_gss_library);
+ list->nlibraries = 0;
/* MIT Kerberos GSSAPI implementation */
/* TODO: For 64-bit builds, check for gssapi64.dll */
- module = LoadLibrary("gssapi32.dll");
+ module = NULL;
+ if (RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\\MIT\\Kerberos", ®key)
+ == ERROR_SUCCESS) {
+ DWORD type, size;
+ LONG ret;
+ char *buffer;
+
+ /* Find out the string length */
+ ret = RegQueryValueEx(regkey, "InstallDir", NULL, &type, NULL, &size);
+
+ if (ret == ERROR_SUCCESS && type == REG_SZ) {
+ buffer = snewn(size + 20, char);
+ ret = RegQueryValueEx(regkey, "InstallDir", NULL,
+ &type, buffer, &size);
+ if (ret == ERROR_SUCCESS && type == REG_SZ) {
+ strcat(buffer, "\\bin\\gssapi32.dll");
+ module = LoadLibrary(buffer);
+ }
+ sfree(buffer);
+ }
+ RegCloseKey(regkey);
+ }
if (module) {
struct ssh_gss_library *lib =
- &ssh_gss_libraries[n_ssh_gss_libraries++];
+ &list->libraries[list->nlibraries++];
lib->id = 0;
lib->gsslogmsg = "Using GSSAPI from GSSAPI32.DLL";
+ lib->handle = (void *)module;
#define BIND_GSS_FN(name) \
lib->u.gssapi.name = (t_gss_##name) GetProcAddress(module, "gss_" #name)
module = load_system32_dll("secur32.dll");
if (module) {
struct ssh_gss_library *lib =
- &ssh_gss_libraries[n_ssh_gss_libraries++];
+ &list->libraries[list->nlibraries++];
lib->id = 1;
lib->gsslogmsg = "Using SSPI from SECUR32.DLL";
+ lib->handle = (void *)module;
GET_WINDOWS_FUNCTION(module, AcquireCredentialsHandleA);
GET_WINDOWS_FUNCTION(module, InitializeSecurityContextA);
ssh_sspi_bind_fns(lib);
}
+
+ /*
+ * Custom GSSAPI DLL.
+ */
+ module = NULL;
+ if (cfg->ssh_gss_custom.path[0]) {
+ module = LoadLibrary(cfg->ssh_gss_custom.path);
+ }
+ if (module) {
+ struct ssh_gss_library *lib =
+ &list->libraries[list->nlibraries++];
+
+ lib->id = 2;
+ lib->gsslogmsg = dupprintf("Using GSSAPI from user-specified"
+ " library '%s'", cfg->ssh_gss_custom.path);
+ lib->handle = (void *)module;
+
+#define BIND_GSS_FN(name) \
+ lib->u.gssapi.name = (t_gss_##name) GetProcAddress(module, "gss_" #name)
+
+ BIND_GSS_FN(delete_sec_context);
+ BIND_GSS_FN(display_status);
+ BIND_GSS_FN(get_mic);
+ BIND_GSS_FN(import_name);
+ BIND_GSS_FN(init_sec_context);
+ BIND_GSS_FN(release_buffer);
+ BIND_GSS_FN(release_cred);
+ BIND_GSS_FN(release_name);
+
+#undef BIND_GSS_FN
+
+ ssh_gssapi_bind_fns(lib);
+ }
+
+
+ return list;
+}
+
+void ssh_gss_cleanup(struct ssh_gss_liblist *list)
+{
+ int i;
+
+ /*
+ * LoadLibrary and FreeLibrary are defined to employ reference
+ * counting in the case where the same library is repeatedly
+ * loaded, so even in a multiple-sessions-per-process context
+ * (not that we currently expect ever to have such a thing on
+ * Windows) it's safe to naively FreeLibrary everything here
+ * without worrying about destroying it under the feet of
+ * another SSH instance still using it.
+ */
+ for (i = 0; i < list->nlibraries; i++) {
+ FreeLibrary((HMODULE)list->libraries[i].handle);
+ if (list->libraries[i].id == 2) {
+ /* The 'custom' id involves a dynamically allocated message.
+ * Note that we must cast away the 'const' to free it. */
+ sfree((char *)list->libraries[i].gsslogmsg);
+ }
+ }
+ sfree(list->libraries);
+ sfree(list);
}
static Ssh_gss_stat ssh_sspi_indicate_mech(struct ssh_gss_library *lib,
#define WINHELP_CTX_ssh_auth_pageant "ssh.auth.pageant:config-ssh-tryagent"
#define WINHELP_CTX_ssh_auth_tis "ssh.auth.tis:config-ssh-tis"
#define WINHELP_CTX_ssh_auth_ki "ssh.auth.ki:config-ssh-ki"
+#define WINHELP_CTX_ssh_gssapi "ssh.auth.gssapi:config-ssh-auth-gssapi"
+#define WINHELP_CTX_ssh_gssapi_delegation "ssh.auth.gssapi.delegation:config-ssh-auth-gssapi-delegation"
+#define WINHELP_CTX_ssh_gssapi_libraries "ssh.auth.gssapi.libraries:config-ssh-auth-gssapi-libraries"
#define WINHELP_CTX_selection_buttons "selection.buttons:config-mouse"
#define WINHELP_CTX_selection_shiftdrag "selection.shiftdrag:config-mouseshift"
#define WINHELP_CTX_selection_rect "selection.rect:config-rectselect"
"All Files (*.*)\0*\0\0\0")
#define FILTER_WAVE_FILES ("Wave Files (*.wav)\0*.WAV\0" \
"All Files (*.*)\0*\0\0\0")
+#define FILTER_DYNLIB_FILES ("Dynamic Library Files (*.dll)\0*.dll\0" \
+ "All Files (*.*)\0*\0\0\0")
/*
* On some versions of Windows, it has been known for WM_TIMER to