diff --git a/CMakeLists.txt b/CMakeLists.txt index 1680223d..213f81af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ add_library(settings STATIC cmdline.c settings.c) add_library(crypto STATIC - cproxy.c) + cproxy.c sshproxy.c) add_subdirectory(crypto) add_library(network STATIC @@ -109,6 +109,7 @@ add_executable(psocks psocks.c norand.c nocproxy.c + nosshproxy.c ssh/portfwd.c) target_link_libraries(psocks eventloop console network utils diff --git a/config.c b/config.c index ca808511..0932f894 100644 --- a/config.c +++ b/config.c @@ -2477,16 +2477,26 @@ void setup_config_box(struct controlbox *b, bool midsession, "Options controlling proxy usage"); s = ctrl_getset(b, "Connection/Proxy", "basics", NULL); - ctrl_radiobuttons(s, "Proxy type:", 't', 3, - HELPCTX(proxy_type), - conf_radiobutton_handler, - I(CONF_proxy_type), - "None", I(PROXY_NONE), - "SOCKS 4", I(PROXY_SOCKS4), - "SOCKS 5", I(PROXY_SOCKS5), - "HTTP", I(PROXY_HTTP), - "Telnet", I(PROXY_TELNET), - NULL); + c = ctrl_radiobuttons(s, "Proxy type:", 't', 3, + HELPCTX(proxy_type), + conf_radiobutton_handler, + I(CONF_proxy_type), + "None", I(PROXY_NONE), + "SOCKS 4", I(PROXY_SOCKS4), + "SOCKS 5", I(PROXY_SOCKS5), + "HTTP", I(PROXY_HTTP), + "Telnet", I(PROXY_TELNET), + NULL); + if (ssh_proxy_supported) { + /* Add an extra radio button to the above list. */ + c->radio.nbuttons++; + c->radio.buttons = + sresize(c->radio.buttons, c->radio.nbuttons, char *); + c->radio.buttons[c->radio.nbuttons-1] = dupstr("SSH"); + c->radio.buttondata = + sresize(c->radio.buttondata, c->radio.nbuttons, intorptr); + c->radio.buttondata[c->radio.nbuttons-1] = I(PROXY_SSH); + } ctrl_columns(s, 2, 80, 20); c = ctrl_editbox(s, "Proxy hostname", 'y', 100, HELPCTX(proxy_main), diff --git a/network.h b/network.h index 89419fb7..92662ac7 100644 --- a/network.h +++ b/network.h @@ -127,6 +127,12 @@ Socket *platform_new_connection(SockAddr *addr, const char *hostname, bool oobinline, bool nodelay, bool keepalive, Plug *plug, Conf *conf); +/* callback for SSH jump-host proxying */ +Socket *sshproxy_new_connection(SockAddr *addr, const char *hostname, + int port, bool privport, + bool oobinline, bool nodelay, bool keepalive, + Plug *plug, Conf *conf); + /* socket functions */ void sk_init(void); /* called once at program startup */ diff --git a/nosshproxy.c b/nosshproxy.c new file mode 100644 index 00000000..e47a860c --- /dev/null +++ b/nosshproxy.c @@ -0,0 +1,16 @@ +/* + * nosshproxy.c: stub implementation of sshproxy_new_connection(). + */ + +#include "putty.h" +#include "network.h" + +const bool ssh_proxy_supported = false; + +Socket *sshproxy_new_connection(SockAddr *addr, const char *hostname, + int port, bool privport, + bool oobinline, bool nodelay, bool keepalive, + Plug *plug, Conf *conf) +{ + return NULL; +} diff --git a/proxy.c b/proxy.c index d7069cb0..ed984e4e 100644 --- a/proxy.c +++ b/proxy.c @@ -405,6 +405,11 @@ Socket *new_connection(SockAddr *addr, const char *hostname, Socket *sret; int type; + if ((sret = sshproxy_new_connection(addr, hostname, port, privport, + oobinline, nodelay, keepalive, + plug, conf)) != NULL) + return sret; + if ((sret = platform_new_connection(addr, hostname, port, privport, oobinline, nodelay, keepalive, plug, conf)) != NULL) diff --git a/putty.h b/putty.h index 75dd5a86..08923ba9 100644 --- a/putty.h +++ b/putty.h @@ -474,7 +474,8 @@ enum { * Proxy types. */ PROXY_NONE, PROXY_SOCKS4, PROXY_SOCKS5, - PROXY_HTTP, PROXY_TELNET, PROXY_CMD, PROXY_FUZZ + PROXY_HTTP, PROXY_TELNET, PROXY_SSH, + PROXY_CMD, PROXY_FUZZ }; enum { @@ -620,6 +621,9 @@ enum { #define BACKEND_SUPPORTS_NC_HOST 0x04 /* Backend can honour CONF_ssh_nc_host */ +/* In (no)sshproxy.c */ +extern const bool ssh_proxy_supported; + struct Backend { const BackendVtable *vt; }; diff --git a/sshproxy.c b/sshproxy.c new file mode 100644 index 00000000..0ffa74ff --- /dev/null +++ b/sshproxy.c @@ -0,0 +1,544 @@ +/* + * sshproxy.c: implement a Socket type that talks to an entire + * subsidiary SSH connection (sometimes called a 'jump host'). + */ + +#include +#include + +#include "putty.h" +#include "ssh.h" +#include "network.h" +#include "storage.h" + +const bool ssh_proxy_supported = true; + +/* + * TODO for future work: + * + * At present, this use of SSH as a proxy is 100% noninteractive. In + * our implementations of the Seat and LogPolicy traits, every method + * that involves interactively prompting the user is implemented by + * pretending the user gave a safe default answer. So the effect is + * very much as if you'd used 'plink -batch' as a proxy subprocess - + * password prompts are cancelled and any dubious host key or crypto + * primitive is unconditionally rejected - except that it all happens + * in-process, making it mildly more convenient to set up, perhaps a + * hair faster, and you get all the Event Log data in one place. + * + * But the biggest benefit of in-process SSH proxying would be that + * the interactive prompts from the sub-SSH can be passed through to + * the end user. If your jump host and your ultimate destination host + * both require password authentication, you should be able to type + * both password in sequence into the PuTTY terminal window; if you're + * running a session of this kind for the first time, you should be + * able to confirm both host keys one after another; if you need to + * store SSH packet logs from both SSH connections, you should be able + * to respond in turn to two askappend() prompts if necessary. And in + * the current state of the code, none of that is yet implemented. + * + * To fix that, we'd have to start by arranging for this proxy + * implementation to get hold of the 'real' (outer) Seat and LogPolicy + * objects, which probably means that they'd have to be passed to + * new_connection. Then, each method in this file that receives an + * interactive prompt request would handle it by passing it on to the + * outer Seat or LogPolicy, with some kind of tweak that would allow + * the end user to see clearly that the prompt had come from the proxy + * SSH connection rather than the primary one. + * + * One problem here is that not all uses of new_connection _have_ a + * Seat or a LogPolicy available. So we'd also have to check if those + * pointers are NULL, and if so, fall back to the existing behaviour + * of behaving as if in batch mode. + */ + +typedef struct SshProxy { + char *errmsg; + Conf *conf; + LogContext *logctx; + Backend *backend; + + ProxyStderrBuf psb; + Plug *plug; + + bool frozen; + bufchain ssh_to_socket; + bool rcvd_eof_ssh_to_socket, sent_eof_ssh_to_socket; + + /* Traits implemented: we're a Socket from the point of view of + * the client connection, and a Seat from the POV of the SSH + * backend we instantiate. */ + Socket sock; + LogPolicy logpolicy; + Seat seat; +} SshProxy; + +static Plug *sshproxy_plug(Socket *s, Plug *p) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + Plug *oldplug = sp->plug; + if (p) + sp->plug = p; + return oldplug; +} + +static void sshproxy_close(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + + sfree(sp->errmsg); + conf_free(sp->conf); + if (sp->backend) + backend_free(sp->backend); + if (sp->logctx) + log_free(sp->logctx); + bufchain_clear(&sp->ssh_to_socket); + + delete_callbacks_for_context(sp); + sfree(sp); +} + +static size_t sshproxy_write(Socket *s, const void *data, size_t len) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + if (!sp->backend) + return 0; + return backend_send(sp->backend, data, len); +} + +static size_t sshproxy_write_oob(Socket *s, const void *data, size_t len) +{ + /* + * oob data is treated as inband; nasty, but nothing really + * better we can do + */ + return sshproxy_write(s, data, len); +} + +static void sshproxy_write_eof(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + if (!sp->backend) + return; + backend_special(sp->backend, SS_EOF, 0); +} + +static void try_send_ssh_to_socket(void *ctx); + +static void sshproxy_set_frozen(Socket *s, bool is_frozen) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + sp->frozen = is_frozen; + if (!sp->frozen) + queue_toplevel_callback(try_send_ssh_to_socket, sp); +} + +static const char *sshproxy_socket_error(Socket *s) +{ + SshProxy *sp = container_of(s, SshProxy, sock); + return sp->errmsg; +} + +static SocketPeerInfo *sshproxy_peer_info(Socket *s) +{ + return NULL; +} + +static const SocketVtable SshProxy_sock_vt = { + .plug = sshproxy_plug, + .close = sshproxy_close, + .write = sshproxy_write, + .write_oob = sshproxy_write_oob, + .write_eof = sshproxy_write_eof, + .set_frozen = sshproxy_set_frozen, + .socket_error = sshproxy_socket_error, + .peer_info = sshproxy_peer_info, +}; + +static void sshproxy_eventlog(LogPolicy *lp, const char *event) +{ + SshProxy *sp = container_of(lp, SshProxy, logpolicy); + log_proxy_stderr(sp->plug, &sp->psb, event, strlen(event)); + log_proxy_stderr(sp->plug, &sp->psb, "\n", 1); +} + +static int sshproxy_askappend(LogPolicy *lp, Filename *filename, + void (*callback)(void *ctx, int result), + void *ctx) +{ + /* + * TODO: if we had access to the outer LogPolicy, we could pass on + * this request to the end user. (But we'd still have to have this + * code as a fallback in case there isn't a LogPolicy available.) + */ + char *msg = dupprintf("Log file \"%s\" already exists; logging cancelled", + filename_to_str(filename)); + sshproxy_eventlog(lp, msg); + sfree(msg); + return 0; +} + +static void sshproxy_logging_error(LogPolicy *lp, const char *event) +{ + /* + * TODO: if we had access to the outer LogPolicy, we could pass on + * this request to _its_ logging_error method, where it would be + * more prominent than just dumping it in the outer SSH + * connection's Event Log. (But we'd still have to have this code + * as a fallback in case there isn't a LogPolicy available.) + */ + char *msg = dupprintf("Logging error: %s", event); + sshproxy_eventlog(lp, msg); + sfree(msg); +} + +static const LogPolicyVtable SshProxy_logpolicy_vt = { + .eventlog = sshproxy_eventlog, + .askappend = sshproxy_askappend, + .logging_error = sshproxy_logging_error, + .verbose = null_lp_verbose_no, +}; + +/* + * Function called when we encounter an error during connection setup that's + * likely to be the cause of terminating the proxy SSH connection. Putting it + * in the Event Log is useful on general principles; also putting it in + * sp->errmsg meaks that it will be passed back through plug_closing when the + * proxy SSH connection actually terminates, so that the end user will see + * what went wrong in the proxy connection. + */ +static void sshproxy_error(SshProxy *sp, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char *msg = dupvprintf(fmt, ap); + va_end(ap); + + if (!sp->errmsg) + sp->errmsg = dupstr(msg); + + sshproxy_eventlog(&sp->logpolicy, msg); + sfree(msg); +} + +static void try_send_ssh_to_socket(void *ctx) +{ + SshProxy *sp = (SshProxy *)ctx; + + if (sp->frozen) + return; + + while (bufchain_size(&sp->ssh_to_socket)) { + ptrlen pl = bufchain_prefix(&sp->ssh_to_socket); + plug_receive(sp->plug, 0, pl.ptr, pl.len); + bufchain_consume(&sp->ssh_to_socket, pl.len); + } + + if (sp->rcvd_eof_ssh_to_socket && + !sp->sent_eof_ssh_to_socket) { + sp->sent_eof_ssh_to_socket = true; + plug_closing(sp->plug, sp->errmsg, 0, 0); + } +} + +static size_t sshproxy_output(Seat *seat, bool is_stderr, + const void *data, size_t len) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + bufchain_add(&sp->ssh_to_socket, data, len); + try_send_ssh_to_socket(sp); + return bufchain_size(&sp->ssh_to_socket); +} + +static bool sshproxy_eof(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + sp->rcvd_eof_ssh_to_socket = true; + try_send_ssh_to_socket(sp); + return false; +} + +static void sshproxy_notify_remote_disconnect(Seat *seat) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (!sp->rcvd_eof_ssh_to_socket && !backend_connected(sp->backend)) + sshproxy_eof(seat); +} + +static int sshproxy_get_userpass_input(Seat *seat, prompts_t *p, + bufchain *input) +{ + /* + * TODO: if we had access to the outer Seat, we could pass on this + * prompts_t to *its* get_userpass_input method, appropriately + * adjusted to indicate that it comes from the proxy SSH + * connection. (But we'd still have to have this code as a + * fallback in case there isn't a Seat available.) + * + * Design question: how does that 'appropriately adjusted' + * interact with the possibility of multiple calls to this + * function with the same prompts_t? Should we redo the + * modification every time? Or provide some kind of callback that + * userauth can use to do it once up front? Or something else? + * + * Also, we'll need to be sure that the outer Seat is in the + * correct trust status before passing prompts along to it. For + * SSH, you'd certainly expect that to be OK, on the basis that + * the primary SSH connection won't set the Seat to untrusted mode + * until it finishes its userauth phase, which won't happen until + * long after _we've_ finished _our_ userauth phase. But what if + * the primary connection is something like Telnet, which goes + * into untrusted mode during startup? We may find we have to do + * some more complicated piece of plumbing that lets us take some + * kind of a preliminary lease on the Seat and defer anything the + * primary backend tries to do to it. + */ + SshProxy *sp = container_of(seat, SshProxy, seat); + sshproxy_error(sp, "Unable to provide interactive authentication " + "requested by proxy SSH connection"); + return 0; +} + +static void sshproxy_connection_fatal_callback(void *vctx) +{ + SshProxy *sp = (SshProxy *)vctx; + plug_closing(sp->plug, sp->errmsg, 0, true); +} + +static void sshproxy_connection_fatal(Seat *seat, const char *message) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + if (!sp->errmsg) { + sp->errmsg = dupprintf( + "fatal error in proxy SSH connection: %s", message); + queue_toplevel_callback(sshproxy_connection_fatal_callback, sp); + } +} + +static int sshproxy_verify_ssh_host_key( + Seat *seat, const char *host, int port, const char *keytype, + char *keystr, const char *keydisp, char **key_fingerprints, + void (*callback)(void *ctx, int result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + /* + * TODO: if we had access to the outer Seat, we could pass on this + * request to *its* verify_ssh_host_key method, appropriately + * adjusted to indicate that it comes from the proxy SSH + * connection. (But we'd still have to have this code as a + * fallback in case there isn't a Seat available.) + * + * Instead, we have to behave as if we're in batch mode: directly + * verify the host key against the cache, and if that fails, take + * the safe option in the absence of interactive confirmation, and + * abort the connection. + */ + int hkstatus = verify_host_key(host, port, keytype, keystr); + FingerprintType fptype = ssh2_pick_default_fingerprint(key_fingerprints); + + switch (hkstatus) { + case 0: /* host key matched */ + return 1; + + case 1: /* host key not in cache at all */ + sshproxy_error(sp, "Host key not in cache for %s:%d (fingerprint %s). " + "Abandoning proxy SSH connection.", host, port, + key_fingerprints[fptype]); + return 0; + + case 2: + sshproxy_error(sp, "HOST KEY DOES NOT MATCH CACHE for %s:%d " + "(fingerprint %s). Abandoning proxy SSH connection.", + host, port, key_fingerprints[fptype]); + return 0; + + default: + unreachable("bad return value from verify_host_key"); + } +} + +static int sshproxy_confirm_weak_crypto_primitive( + Seat *seat, const char *algtype, const char *algname, + void (*callback)(void *ctx, int result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + /* + * TODO: if we had access to the outer Seat, we could pass on this + * request to *its* confirm_weak_crypto_primitive method, + * appropriately adjusted to indicate that it comes from the proxy + * SSH connection. (But we'd still have to have this code as a + * fallback in case there isn't a Seat available.) + */ + sshproxy_error(sp, "First %s supported by server is %s, below warning " + "threshold. Abandoning proxy SSH connection.", + algtype, algname); + return 0; +} + +static int sshproxy_confirm_weak_cached_hostkey( + Seat *seat, const char *algname, const char *betteralgs, + void (*callback)(void *ctx, int result), void *ctx) +{ + SshProxy *sp = container_of(seat, SshProxy, seat); + + /* + * TODO: if we had access to the outer Seat, we could pass on this + * request to *its* confirm_weak_cached_hostkey method, + * appropriately adjusted to indicate that it comes from the proxy + * SSH connection. (But we'd still have to have this code as a + * fallback in case there isn't a Seat available.) + */ + sshproxy_error(sp, "First host key type stored for server is %s, below " + "warning threshold. Abandoning proxy SSH connection.", + algname); + return 0; +} + +static bool sshproxy_set_trust_status(Seat *seat, bool trusted) +{ + /* + * This is called by the proxy SSH connection, to set our Seat + * into a given trust status. We can safely do nothing here and + * return true to claim we did something (effectively eliminating + * the spoofing defences completely, by suppressing the 'press + * Return to begin session' prompt and not providing anything in + * place of it), on the basis that session I/O from the proxy SSH + * connection is never passed directly on to the end user, so a + * malicious proxy SSH server wouldn't be able to spoof our human + * in any case. + */ + return true; +} + +static const SeatVtable SshProxy_seat_vt = { + .output = sshproxy_output, + .eof = sshproxy_eof, + .get_userpass_input = sshproxy_get_userpass_input, + .notify_remote_exit = nullseat_notify_remote_exit, + .notify_remote_disconnect = sshproxy_notify_remote_disconnect, + .connection_fatal = sshproxy_connection_fatal, + .update_specials_menu = nullseat_update_specials_menu, + .get_ttymode = nullseat_get_ttymode, + .set_busy_status = nullseat_set_busy_status, + .verify_ssh_host_key = sshproxy_verify_ssh_host_key, + .confirm_weak_crypto_primitive = sshproxy_confirm_weak_crypto_primitive, + .confirm_weak_cached_hostkey = sshproxy_confirm_weak_cached_hostkey, + .is_utf8 = nullseat_is_never_utf8, + .echoedit_update = nullseat_echoedit_update, + .get_x_display = nullseat_get_x_display, + .get_windowid = nullseat_get_windowid, + .get_window_pixel_size = nullseat_get_window_pixel_size, + .stripctrl_new = nullseat_stripctrl_new, + .set_trust_status = sshproxy_set_trust_status, + .verbose = nullseat_verbose_no, + .interactive = nullseat_interactive_no, + .get_cursor_position = nullseat_get_cursor_position, + +}; + +Socket *sshproxy_new_connection(SockAddr *addr, const char *hostname, + int port, bool privport, + bool oobinline, bool nodelay, bool keepalive, + Plug *plug, Conf *clientconf) +{ + SshProxy *sp = snew(SshProxy); + memset(sp, 0, sizeof(*sp)); + + sp->sock.vt = &SshProxy_sock_vt; + sp->logpolicy.vt = &SshProxy_logpolicy_vt; + sp->seat.vt = &SshProxy_seat_vt; + sp->plug = plug; + psb_init(&sp->psb); + bufchain_init(&sp->ssh_to_socket); + + sp->conf = conf_new(); + /* Try to treat proxy_hostname as the title of a saved session. If + * that fails, set up a default Conf of our own treating it as a + * hostname. */ + const char *proxy_hostname = conf_get_str(clientconf, CONF_proxy_host); + if (do_defaults(proxy_hostname, sp->conf)) { + if (!conf_launchable(sp->conf)) { + sp->errmsg = dupprintf("saved session '%s' is not launchable", + proxy_hostname); + return &sp->sock; + } + } else { + do_defaults(NULL, sp->conf); + /* In hostname mode, we default to PROT_SSH. This is more useful than + * the obvious approach of defaulting to the protocol defined in + * Default Settings, because only SSH (ok, and bare ssh-connection) + * can be used for this kind of proxy. */ + conf_set_int(sp->conf, CONF_protocol, PROT_SSH); + conf_set_str(sp->conf, CONF_host, proxy_hostname); + conf_set_int(sp->conf, CONF_port, + conf_get_int(clientconf, CONF_proxy_port)); + } + const char *proxy_username = conf_get_str(clientconf, CONF_proxy_username); + if (*proxy_username) + conf_set_str(sp->conf, CONF_username, proxy_username); + + const struct BackendVtable *backvt = backend_vt_from_proto( + conf_get_int(sp->conf, CONF_protocol)); + + /* + * We don't actually need an _SSH_ session specifically: it's also + * OK to use PROT_SSHCONN, because really, the criterion is + * whether setting CONF_ssh_nc_host will do anything useful. So + * our check is for whether the backend sets the flag promising + * that it does. + */ + if (!(backvt->flags & BACKEND_SUPPORTS_NC_HOST)) { + sp->errmsg = dupprintf("saved session '%s' is not an SSH session", + proxy_hostname); + return &sp->sock; + } + + /* + * Turn off SSH features we definitely don't want. It would be + * awkward and counterintuitive to have the proxy SSH connection + * become a connection-sharing upstream (but it's fine to have it + * be a downstream, if that's configured). And we don't want to + * open X forwardings, agent forwardings or (other) port + * forwardings as a side effect of this one operation. + */ + conf_set_bool(sp->conf, CONF_ssh_connection_sharing_upstream, false); + conf_set_bool(sp->conf, CONF_x11_forward, false); + conf_set_bool(sp->conf, CONF_agentfwd, false); + for (const char *subkey; + (subkey = conf_get_str_nthstrkey(sp->conf, CONF_portfwd, 0)) != NULL;) + conf_del_str_str(sp->conf, CONF_portfwd, subkey); + + /* + * We'll only be running one channel through this connection + * (since we've just turned off all the other things we might have + * done with it), so we can configure it as simple. + */ + conf_set_bool(sp->conf, CONF_ssh_simple, true); + + /* + * Configure the main channel of this SSH session to be a + * direct-tcpip connection to the destination host/port. + */ + conf_set_str(sp->conf, CONF_ssh_nc_host, hostname); + conf_set_int(sp->conf, CONF_ssh_nc_port, port); + + sp->logctx = log_init(&sp->logpolicy, sp->conf); + + char *error, *realhost; + error = backend_init(backvt, &sp->seat, &sp->backend, sp->logctx, sp->conf, + conf_get_str(sp->conf, CONF_host), + conf_get_int(sp->conf, CONF_port), + &realhost, nodelay, + conf_get_bool(sp->conf, CONF_tcp_keepalives)); + if (error) { + sp->errmsg = dupprintf("unable to open SSH proxy connection: %s", + error); + return &sp->sock; + } + + sfree(realhost); + + return &sp->sock; +} diff --git a/unix/CMakeLists.txt b/unix/CMakeLists.txt index f724bbe7..7ce5e669 100644 --- a/unix/CMakeLists.txt +++ b/unix/CMakeLists.txt @@ -137,7 +137,8 @@ if(GTK_FOUND) askpass.c x11.c noise.c - ${CMAKE_SOURCE_DIR}/ssh/x11fwd.c) + ${CMAKE_SOURCE_DIR}/ssh/x11fwd.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c) target_link_libraries(pageant eventloop console agent settings network crypto utils ${GTK_LIBRARIES}) @@ -148,6 +149,7 @@ if(GTK_FOUND) main-gtk-simple.c ${CMAKE_SOURCE_DIR}/be_none.c ${CMAKE_SOURCE_DIR}/nogss.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c pty.c) target_link_libraries(pterm guiterminal eventloop settings charset utils @@ -160,6 +162,7 @@ if(GTK_FOUND) ${CMAKE_SOURCE_DIR}/nocmdline.c ${CMAKE_SOURCE_DIR}/be_none.c ${CMAKE_SOURCE_DIR}/nogss.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c pty.c) target_link_libraries(ptermapp guiterminal eventloop settings charset utils @@ -193,7 +196,8 @@ if(GTK_FOUND) ${CMAKE_SOURCE_DIR}/be_nos_s.c ${CMAKE_SOURCE_DIR}/nogss.c ${CMAKE_SOURCE_DIR}/norand.c - ${CMAKE_SOURCE_DIR}/nocproxy.c) + ${CMAKE_SOURCE_DIR}/nocproxy.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c) target_link_libraries(puttytel guiterminal eventloop otherbackends settings network charset utils ${GTK_LIBRARIES} ${X11_LIBRARIES}) diff --git a/unix/config-unix.c b/unix/config-unix.c index 8397a0ac..7efff05d 100644 --- a/unix/config-unix.c +++ b/unix/config-unix.c @@ -51,6 +51,8 @@ void unix_setup_config_box(struct controlbox *b, bool midsession, int protocol) c->radio.buttondata = sresize(c->radio.buttondata, c->radio.nbuttons, intorptr); c->radio.buttondata[c->radio.nbuttons-1] = I(PROXY_CMD); + if (c->radio.ncolumns < 4) + c->radio.ncolumns = 4; break; } } diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index a46c9a4b..1daf926e 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -104,6 +104,7 @@ add_executable(puttytel ${CMAKE_SOURCE_DIR}/nogss.c ${CMAKE_SOURCE_DIR}/norand.c ${CMAKE_SOURCE_DIR}/nocproxy.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c puttytel.rc) add_dependencies(puttytel generated_licence_h) target_link_libraries(puttytel @@ -143,6 +144,7 @@ if(HAVE_CONPTY) be_conpty.c ${CMAKE_SOURCE_DIR}/nogss.c ${CMAKE_SOURCE_DIR}/norand.c + ${CMAKE_SOURCE_DIR}/nosshproxy.c pterm.rc) add_dependencies(pterm generated_licence_h) target_link_libraries(pterm diff --git a/windows/config.c b/windows/config.c index fab3240f..a32944a6 100644 --- a/windows/config.c +++ b/windows/config.c @@ -374,6 +374,8 @@ void win_setup_config_box(struct controlbox *b, HWND *hwndp, bool has_help, c->radio.buttondata = sresize(c->radio.buttondata, c->radio.nbuttons, intorptr); c->radio.buttondata[c->radio.nbuttons-1] = I(PROXY_CMD); + if (c->radio.ncolumns < 4) + c->radio.ncolumns = 4; break; } }